From ac6d29576cdcf17e8677a75691904595ec0c2548 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 15:49:39 +0000 Subject: [PATCH 01/35] test(fixtures): isolate root pytest plugins and migrate db2 to clientless service --- pyproject.toml | 6 +- tests/conftest.py | 78 +- tests/integration/conftest.py | 70 +- tests/integration/fixtures/__init__.py | 213 +- tests/integration/fixtures/asyncpg_adk.py | 69 - tests/integration/fixtures/db2.py | 76 + tests/plugins/__init__.py | 1 - tests/plugins/db2.py | 315 --- tests/unit/test_import_time.py | 43 +- uv.lock | 2144 ++++++++++++--------- 10 files changed, 1404 insertions(+), 1611 deletions(-) delete mode 100644 tests/integration/fixtures/asyncpg_adk.py create mode 100644 tests/integration/fixtures/db2.py delete mode 100644 tests/plugins/__init__.py delete mode 100644 tests/plugins/db2.py diff --git a/pyproject.toml b/pyproject.toml index 3f3d3aa90..328731760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,6 @@ lint = [ "types-Pygments", "types-colorama", "types-cffi", - "types-docker", "types-protobuf", "asyncpg-stubs", "pyarrow-stubs", @@ -133,7 +132,7 @@ test = [ "pytest>=8.0.0", "pytest-cov>=5.0.0", - "pytest-databases[postgres,oracle,bigquery,spanner]>=0.19.0", + "pytest-databases>=0.19.0", "pytest-mock>=3.14.0", "pytest-sugar>=1.0.0", "pytest-xdist>=3.6.1", @@ -184,6 +183,9 @@ pytest-databases = false sqlglot = false sqlglotc = false +[tool.uv.sources] +pytest-databases = { git = "https://github.com/litestar-org/pytest-databases.git", branch = "test/clientless-all" } + [build-system] build-backend = "hatchling.build" requires = ["hatchling", "hatch-mypyc"] diff --git a/tests/conftest.py b/tests/conftest.py index 4fb5ae962..70aea1233 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,6 @@ +"""Root pytest configuration and shared test hooks.""" + +import functools import importlib.machinery import logging import os @@ -24,37 +27,13 @@ }.items(): logging.getLogger(_logger_name).setLevel(_level) - -pytest_plugins = [ - "pytest_databases.docker.postgres", - "pytest_databases.docker.oracle", - "pytest_databases.docker.mysql", - "pytest_databases.docker.mssql", - "pytest_databases.docker.bigquery", - "pytest_databases.docker.spanner", - "pytest_databases.docker.gizmosql", - "pytest_databases.docker.cockroachdb", - "pytest_databases.docker.rustfs", - "tests.plugins.db2", - "tests.integration.fixtures", - "tests.integration.fixtures.adapter_cases", - "tests.integration.fixtures.aiomysql_adk", - "tests.integration.fixtures.asyncmy_adk", - "tests.integration.fixtures.asyncpg_adk", - "tests.integration.fixtures.mysqlconnector_adk", - "tests.integration.fixtures.oracle_events", - "tests.integration.fixtures.spanner_adk", - "tests.integration.fixtures.spanner_events", - "tests.integration.fixtures.spanner_litestar", -] - pytestmark = pytest.mark.anyio here = Path(__file__).parent - COMPILED_EXTENSION_SUFFIXES: tuple[str, ...] = tuple( dict.fromkeys((*importlib.machinery.EXTENSION_SUFFIXES, ".so", ".pyd")) ) +_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"}) def is_compiled() -> bool: @@ -93,24 +72,18 @@ def pytest_report_header(config: pytest.Config) -> list[str]: ) -def _module_is_available(module: str) -> bool: - try: - return find_spec(module) is not None - except ModuleNotFoundError: - return False - - -_MSSQL_PYTHON_AVAILABLE = _module_is_available("mssql_python") -_SPANNER_AVAILABLE = _module_is_available("google.cloud.spanner_v1") +def should_run_optional_cloud_suite(cli_enabled: bool, env_var: str) -> bool: + """Return True when an optional cloud emulator suite is enabled via CI, CLI flag, or env var.""" + return os.getenv("CI") == "true" or bool(cli_enabled) or _env_flag_enabled(env_var) def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None: """Skip optional adapter unit trees before importing unavailable dependencies.""" _ = config path = str(collection_path) - if "tests/unit/adapters/test_mssql_python" in path and not _MSSQL_PYTHON_AVAILABLE: + if "tests/unit/adapters/test_mssql_python" in path and not _module_is_available("mssql_python"): return True - if "tests/unit/adapters/test_spanner" in path and not _SPANNER_AVAILABLE: + if "tests/unit/adapters/test_spanner" in path and not _module_is_available("google.cloud.spanner_v1"): return True return None @@ -121,29 +94,29 @@ def pytest_addoption(parser: pytest.Parser) -> None: "--run-bigquery-tests", action="store_true", default=False, - help="Run BigQuery integration tests locally (enabled by default in CI; otherwise requires SQLSPEC_ENABLE_BIGQUERY_TESTS=1)", + help="Run BigQuery integration tests locally (enabled by default in CI; local runs accept --run-bigquery-tests or SQLSPEC_ENABLE_BIGQUERY_TESTS=1)", ) parser.addoption( "--run-spanner-tests", action="store_true", default=False, - help="Run Spanner integration tests locally (enabled by default in CI; otherwise requires SQLSPEC_ENABLE_SPANNER_TESTS=1)", + help="Run Spanner integration tests locally (enabled by default in CI; local runs accept --run-spanner-tests or SQLSPEC_ENABLE_SPANNER_TESTS=1)", ) def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: """Apply environment-sensitive collection skips.""" - bigquery_enabled = os.getenv("CI") == "true" or bool( - config.getoption("--run-bigquery-tests", default=False) and os.getenv("SQLSPEC_ENABLE_BIGQUERY_TESTS") == "1" + bigquery_enabled = should_run_optional_cloud_suite( + bool(config.getoption("--run-bigquery-tests", default=False)), "SQLSPEC_ENABLE_BIGQUERY_TESTS" ) - spanner_enabled = os.getenv("CI") == "true" or bool( - config.getoption("--run-spanner-tests", default=False) and os.getenv("SQLSPEC_ENABLE_SPANNER_TESTS") == "1" + spanner_enabled = should_run_optional_cloud_suite( + bool(config.getoption("--run-spanner-tests", default=False)), "SQLSPEC_ENABLE_SPANNER_TESTS" ) skip_bigquery = pytest.mark.skip( - reason="BigQuery integration tests run by default in CI; local runs require SQLSPEC_ENABLE_BIGQUERY_TESTS=1 and --run-bigquery-tests" + reason="BigQuery integration tests run by default in CI; local runs accept --run-bigquery-tests or SQLSPEC_ENABLE_BIGQUERY_TESTS=1" ) skip_spanner = pytest.mark.skip( - reason="Spanner integration tests run by default in CI; local runs require SQLSPEC_ENABLE_SPANNER_TESTS=1 and --run-spanner-tests" + reason="Spanner integration tests run by default in CI; local runs accept --run-spanner-tests or SQLSPEC_ENABLE_SPANNER_TESTS=1" ) for item in items: item_path = str(getattr(item, "path", getattr(item, "fspath", ""))) @@ -166,7 +139,7 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item @pytest.fixture(scope="session", autouse=True) -def disable_spanner_builtin_metrics() -> "Generator[None, None, None]": +def disable_spanner_builtin_metrics() -> Generator[None, None, None]: """Disable Spanner built-in metrics export during tests.""" if os.getenv("SPANNER_DISABLE_BUILTIN_METRICS") is None: os.environ["SPANNER_DISABLE_BUILTIN_METRICS"] = "true" @@ -188,6 +161,7 @@ def anyio_backend() -> str: @pytest.fixture(autouse=True) def disable_sync_to_thread_warning(monkeypatch: pytest.MonkeyPatch) -> None: + """Suppress Litestar implicit sync-to-thread warnings during tests.""" monkeypatch.setenv("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD", "0") @@ -197,3 +171,17 @@ def clear_sql_caches() -> Generator[None, None, None]: clear_all_caches() yield clear_all_caches() + + +@functools.cache +def _module_is_available(module: str) -> bool: + """Return True when a module specification can be resolved without importing it.""" + try: + return find_spec(module) is not None + except ModuleNotFoundError: + return False + + +def _env_flag_enabled(name: str) -> bool: + """Return True when an environment variable holds a truthy opt-in value.""" + return os.getenv(name, "").strip().lower() in _TRUTHY_ENV_VALUES diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f092abc59..d7cdd76a3 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,23 +1,79 @@ """Pytest configuration and fixtures for integration tests.""" +import pkgutil +from importlib.util import find_spec from typing import TYPE_CHECKING import pytest -from pytest_databases.types import XdistIsolationLevel -from tests.fixtures.rustfs import ensure_rustfs_bucket +import tests.integration.fixtures as fixtures_pkg if TYPE_CHECKING: from pytest_databases.docker.rustfs import RustfsService +pytest_plugins = [ + "pytest_databases.docker.bigquery", + "pytest_databases.docker.cockroachdb", + "pytest_databases.docker.db2", + "pytest_databases.docker.gizmosql", + "pytest_databases.docker.mssql", + "pytest_databases.docker.mysql", + "pytest_databases.docker.oracle", + "pytest_databases.docker.postgres", + "pytest_databases.docker.rustfs", + "pytest_databases.docker.spanner", +] -@pytest.fixture(scope="session") -def xdist_rustfs_isolation_level() -> XdistIsolationLevel: - """Run one transient RustFS service per worker that exercises object storage.""" - return "server" +_NON_PLUGIN_FIXTURE_MODULES: frozenset[str] = frozenset({"cleanup"}) + +_FIXTURE_DRIVER_REQUIREMENTS: dict[str, tuple[tuple[str, ...], ...]] = { + "adbc": (("adbc_driver_manager", "pyarrow"),), + "adapter_cases": ((),), + "bigquery": (("google.cloud.bigquery",),), + "cockroach": (("asyncpg",), ("psycopg", "psycopg_pool")), + "db2": (("ibm_db",),), + "duckdb": (("duckdb",),), + "mssql": (("arrow_odbc", "pyarrow"), ("mssql_python",), ("pymssql",)), + "mysql": (("aiomysql",), ("asyncmy",), ("mysql.connector",), ("pymysql",)), + "oracle": (("oracledb",),), + "oracle_events": (("oracledb",),), + "postgres": (("adbc_driver_manager", "pyarrow"), ("asyncpg",), ("psqlpy",), ("psycopg", "psycopg_pool")), + "spanner": (("google.cloud.spanner",),), + "spanner_adk": (("google.cloud.spanner",),), + "spanner_events": (("google.cloud.spanner",),), + "spanner_litestar": (("google.cloud.spanner", "litestar"),), + "sqlite": ((),), +} + + +def pytest_configure(config: pytest.Config) -> None: + """Import integration fixture plugins whose backing driver packages are installed.""" + for module_info in sorted(pkgutil.iter_modules(fixtures_pkg.__path__), key=lambda item: item.name): + if module_info.name in _NON_PLUGIN_FIXTURE_MODULES: + continue + requirements = _FIXTURE_DRIVER_REQUIREMENTS.get(module_info.name) + if requirements is None or not _requirements_satisfied(requirements): + continue + plugin_name = f"{fixtures_pkg.__name__}.{module_info.name}" + if not config.pluginmanager.has_plugin(plugin_name): + config.pluginmanager.import_plugin(plugin_name) @pytest.fixture(scope="session") def rustfs_bucket_name(rustfs_service: "RustfsService", rustfs_default_bucket_name: str) -> str: """Return the verified RustFS bucket used by S3-compatible integration tests.""" - return ensure_rustfs_bucket(rustfs_service, rustfs_default_bucket_name) + _ = rustfs_service + return rustfs_default_bucket_name + + +def _package_available(package: str) -> bool: + """Return True when a top-level or dotted package specification can be resolved.""" + try: + return find_spec(package) is not None + except ModuleNotFoundError: + return False + + +def _requirements_satisfied(requirements: tuple[tuple[str, ...], ...]) -> bool: + """Return True when at least one required driver package group is importable.""" + return any(all(_package_available(package) for package in group) for group in requirements) diff --git a/tests/integration/fixtures/__init__.py b/tests/integration/fixtures/__init__.py index e31011183..3e96af5c7 100644 --- a/tests/integration/fixtures/__init__.py +++ b/tests/integration/fixtures/__init__.py @@ -1,212 +1,3 @@ -"""Shared integration fixture definitions.""" +"""Integration test fixture modules.""" -from tests.integration.fixtures.adbc import ( - adbc_duckdb_config, - adbc_duckdb_driver, - adbc_duckdb_session, - adbc_gizmosql_config, - adbc_gizmosql_connection_config, - adbc_gizmosql_session, - adbc_gizmosql_sqlite_config, - adbc_gizmosql_sqlite_connection_config, - adbc_gizmosql_sqlite_session, - adbc_postgresql_config, - adbc_postgresql_session, - adbc_sqlite_config, - adbc_sqlite_session, - gizmosql_sqlite_service, - xdist_gizmosql_isolation_level, -) -from tests.integration.fixtures.bigquery import ( - bigquery_config, - bigquery_session, - native_bigquery_service, - table_schema_prefix, -) -from tests.integration.fixtures.duckdb import duckdb_basic_config, duckdb_basic_session -from tests.integration.fixtures.mssql import ( - arrow_odbc_mssql_config, - contract_mssql_python_store, - contract_pymssql_store, - events_config_mssql_python, - events_config_pymssql, - mssql_migration_connection_config, - mssql_python_config, - mssql_python_connection_config, - mssql_python_migration_connection_config, - pymssql_config, - pymssql_connection_config, -) -from tests.integration.fixtures.mysql import ( - aiomysql_clean_driver, - aiomysql_config, - aiomysql_driver, - asyncmy_clean_driver, - asyncmy_config, - asyncmy_driver, - mysqlconnector_async_config, - mysqlconnector_async_driver, - mysqlconnector_clean_async_driver, - mysqlconnector_clean_sync_driver, - mysqlconnector_sync_config, - mysqlconnector_sync_driver, - mysqlconnector_sync_transaction_config, - pymysql_clean_driver, - pymysql_config, - pymysql_driver, - pymysql_transaction_config, -) -from tests.integration.fixtures.oracle import ( - oracle_18c_async_config, - oracle_18c_async_session, - oracle_18c_connection_config, - oracle_aq_privileges, - oracle_async_config, - oracle_async_session, - oracle_connection_config, - oracle_sync_config, - oracle_sync_session, -) -from tests.integration.fixtures.postgres import ( - adbc_postgres_config, - adbc_postgres_connection_config, - adbc_sync_driver, - asyncpg_async_driver, - asyncpg_config, - asyncpg_connection_config, - cockroach_asyncpg_config, - cockroach_psycopg_async_config, - cockroach_psycopg_sync_config, - paradedb_config_adbc, - paradedb_config_asyncpg, - paradedb_config_psqlpy, - paradedb_config_psycopg, - pgvector_config_adbc, - pgvector_config_asyncpg, - pgvector_config_psqlpy, - pgvector_config_psycopg, - psqlpy_config, - psqlpy_driver, - psqlpy_session, - psycopg_async_config, - psycopg_sync_config, -) -from tests.integration.fixtures.spanner import ( - spanner_async_config, - spanner_config, - spanner_database, - spanner_read_session, - spanner_session, - spanner_write_session, -) -from tests.integration.fixtures.sqlite import ( - aiosqlite_config, - aiosqlite_config_file, - aiosqlite_session, - aiosqlite_session_config, - sqlite_basic_session, - sqlite_config_regular_memory, - sqlite_config_shared_memory, - sqlite_driver, - sqlite_session, - sqlite_temp_file_config, -) - -__all__ = ( - "adbc_duckdb_config", - "adbc_duckdb_driver", - "adbc_duckdb_session", - "adbc_gizmosql_config", - "adbc_gizmosql_connection_config", - "adbc_gizmosql_session", - "adbc_gizmosql_sqlite_config", - "adbc_gizmosql_sqlite_connection_config", - "adbc_gizmosql_sqlite_session", - "adbc_postgres_config", - "adbc_postgres_connection_config", - "adbc_postgresql_config", - "adbc_postgresql_session", - "adbc_sqlite_config", - "adbc_sqlite_session", - "adbc_sync_driver", - "aiomysql_clean_driver", - "aiomysql_config", - "aiomysql_driver", - "aiosqlite_config", - "aiosqlite_config_file", - "aiosqlite_session", - "aiosqlite_session_config", - "arrow_odbc_mssql_config", - "asyncmy_clean_driver", - "asyncmy_config", - "asyncmy_driver", - "asyncpg_async_driver", - "asyncpg_config", - "asyncpg_connection_config", - "bigquery_config", - "bigquery_session", - "cockroach_asyncpg_config", - "cockroach_psycopg_async_config", - "cockroach_psycopg_sync_config", - "contract_mssql_python_store", - "contract_pymssql_store", - "duckdb_basic_config", - "duckdb_basic_session", - "events_config_mssql_python", - "events_config_pymssql", - "gizmosql_sqlite_service", - "mssql_migration_connection_config", - "mssql_python_config", - "mssql_python_connection_config", - "mssql_python_migration_connection_config", - "mysqlconnector_async_config", - "mysqlconnector_async_driver", - "mysqlconnector_clean_async_driver", - "mysqlconnector_clean_sync_driver", - "mysqlconnector_sync_config", - "mysqlconnector_sync_driver", - "mysqlconnector_sync_transaction_config", - "native_bigquery_service", - "oracle_18c_async_config", - "oracle_18c_async_session", - "oracle_18c_connection_config", - "oracle_aq_privileges", - "oracle_async_config", - "oracle_async_session", - "oracle_connection_config", - "oracle_sync_config", - "oracle_sync_session", - "paradedb_config_adbc", - "paradedb_config_asyncpg", - "paradedb_config_psqlpy", - "paradedb_config_psycopg", - "pgvector_config_adbc", - "pgvector_config_asyncpg", - "pgvector_config_psqlpy", - "pgvector_config_psycopg", - "psqlpy_config", - "psqlpy_driver", - "psqlpy_session", - "psycopg_async_config", - "psycopg_sync_config", - "pymssql_config", - "pymssql_connection_config", - "pymysql_clean_driver", - "pymysql_config", - "pymysql_driver", - "pymysql_transaction_config", - "spanner_async_config", - "spanner_config", - "spanner_database", - "spanner_read_session", - "spanner_session", - "spanner_write_session", - "sqlite_basic_session", - "sqlite_config_regular_memory", - "sqlite_config_shared_memory", - "sqlite_driver", - "sqlite_session", - "sqlite_temp_file_config", - "table_schema_prefix", - "xdist_gizmosql_isolation_level", -) +__all__: tuple[str, ...] = () diff --git a/tests/integration/fixtures/asyncpg_adk.py b/tests/integration/fixtures/asyncpg_adk.py deleted file mode 100644 index 25dd4e0c1..000000000 --- a/tests/integration/fixtures/asyncpg_adk.py +++ /dev/null @@ -1,69 +0,0 @@ -"""AsyncPG ADK test fixtures.""" - -from collections.abc import AsyncGenerator - -import pytest -from pytest_databases.docker.postgres import PostgresService - -from sqlspec.adapters.asyncpg import AsyncpgConfig -from sqlspec.adapters.asyncpg.adk import AsyncpgADKStore - - -@pytest.fixture -async def asyncpg_adk_store(postgres_service: "PostgresService") -> "AsyncGenerator[AsyncpgADKStore, None]": - """Create AsyncPG ADK store with test database. - - Args: - postgres_service: Pytest fixture providing PostgreSQL connection config. - - Yields: - Configured AsyncPG ADK store instance. - - Notes: - Uses pytest-databases PostgreSQL container for testing. - Tables are created before test and cleaned up after. - Pool is properly closed to avoid threading issues. - """ - config = AsyncpgConfig( - connection_config={ - "host": postgres_service.host, - "port": postgres_service.port, - "user": postgres_service.user, - "password": postgres_service.password, - "database": postgres_service.database, - "max_size": 20, - "min_size": 5, - } - ) - - try: - store = AsyncpgADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn: - await conn.execute("DROP TABLE IF EXISTS adk_events CASCADE") - await conn.execute("DROP TABLE IF EXISTS adk_sessions CASCADE") - finally: - if config.connection_instance: - await config.close_pool() - config.connection_instance = None - - -@pytest.fixture -async def session_fixture(asyncpg_adk_store: AsyncpgADKStore) -> dict[str, str]: - """Create a test session. - - Args: - asyncpg_adk_store: AsyncPG ADK store fixture. - - Returns: - Dictionary with session metadata. - """ - session_id = "test-session" - app_name = "test-app" - user_id = "user-123" - state = {"test": True} - await asyncpg_adk_store.create_session(session_id, app_name, user_id, state) - return {"session_id": session_id, "app_name": app_name, "user_id": user_id} diff --git a/tests/integration/fixtures/db2.py b/tests/integration/fixtures/db2.py new file mode 100644 index 000000000..8a6a86fea --- /dev/null +++ b/tests/integration/fixtures/db2.py @@ -0,0 +1,76 @@ +"""IBM Db2 integration test fixtures.""" + +import os +from collections.abc import AsyncGenerator, Generator +from typing import TYPE_CHECKING + +import pytest + +from sqlspec.adapters.db2.config import Db2AsyncConfig, Db2ConnectionParams, Db2SyncConfig + +if TYPE_CHECKING: + from pytest_databases.docker.db2 import Db2Service + + from sqlspec.adapters.db2.driver import Db2AsyncDriver, Db2SyncDriver + +__all__ = ( + "db2_async_config", + "db2_async_session", + "db2_connection_config", + "db2_image", + "db2_session", + "db2_sync_config", +) + + +@pytest.fixture(scope="session") +def db2_image() -> str: + """Return the pinned IBM Db2 Community Edition container image.""" + return os.environ.get("DB2_IMAGE", "icr.io/db2_community/db2:11.5.9.0") + + +@pytest.fixture(scope="session") +def db2_connection_config(db2_service: "Db2Service") -> Db2ConnectionParams: + """Build SQLSpec Db2 connection parameters from the Db2 service container.""" + return { + "database": db2_service.db_name, + "hostname": db2_service.host, + "port": db2_service.port, + "protocol": "TCPIP", + "user": db2_service.username, + "password": db2_service.password, + } + + +@pytest.fixture(scope="session") +def db2_sync_config(db2_connection_config: Db2ConnectionParams) -> Generator[Db2SyncConfig, None, None]: + """Yield a session-scoped Db2SyncConfig and close its connection pool on teardown.""" + config = Db2SyncConfig(connection_config=dict(db2_connection_config)) + try: + yield config + finally: + config.close_pool() + + +@pytest.fixture(scope="session") +async def db2_async_config(db2_connection_config: Db2ConnectionParams) -> AsyncGenerator[Db2AsyncConfig, None]: + """Yield a session-scoped Db2AsyncConfig and close its connection pool on teardown.""" + config = Db2AsyncConfig(connection_config=dict(db2_connection_config)) + try: + yield config + finally: + await config.close_pool() + + +@pytest.fixture +def db2_session(db2_sync_config: Db2SyncConfig) -> "Generator[Db2SyncDriver, None, None]": + """Yield a function-scoped Db2 synchronous driver session.""" + with db2_sync_config.provide_session() as driver: + yield driver + + +@pytest.fixture +async def db2_async_session(db2_async_config: Db2AsyncConfig) -> "AsyncGenerator[Db2AsyncDriver, None]": + """Yield a function-scoped Db2 asynchronous driver session.""" + async with db2_async_config.provide_session() as driver: + yield driver diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py deleted file mode 100644 index 595741834..000000000 --- a/tests/plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test plugins package.""" diff --git a/tests/plugins/db2.py b/tests/plugins/db2.py deleted file mode 100644 index 05d5ea34e..000000000 --- a/tests/plugins/db2.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Pytest Docker service extension for IBM Db2.""" - -import contextlib -import socket -import tempfile -import time -from collections.abc import AsyncGenerator, Generator -from dataclasses import dataclass -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import filelock -import ibm_db_dbi -import pytest -from docker.errors import NotFound -from pytest_databases._service import get_docker_client -from pytest_databases.helpers import get_xdist_worker_num -from pytest_databases.types import ServiceContainer - -from sqlspec.adapters.db2.config import Db2AsyncConfig, Db2SyncConfig - -if TYPE_CHECKING: - from docker import DockerClient - from docker.models.containers import Container - from pytest_databases._service import DockerService - - from sqlspec.adapters.db2.driver import Db2AsyncDriver, Db2SyncDriver - -DB2_CONTAINER_PORT = 50000 -DB2_READY_TIMEOUT = 600 -DB2_READY_INTERVAL = 5.0 - - -def db2_responsive(host: str, port: int, database: str, user: str, password: str) -> bool: - """Report whether Db2 accepts a connection and answers a query. - - Args: - host: Database host name. - port: Database TCP port. - database: Database name. - user: User name. - password: User password. - - Returns: - ``True`` once ``SELECT 1 FROM SYSIBM.SYSDUMMY1`` returns 1; ``False`` while the port is - closed or Db2 refuses the connection or query. - """ - try: - with socket.create_connection((host, port), timeout=1.0): - pass - except OSError: - return False - - dsn = f"DATABASE={database};HOSTNAME={host};PORT={port};PROTOCOL=TCPIP;UID={user};PWD={password};" - try: - connection = ibm_db_dbi.connect(dsn, "", "", "", "", None) - except ibm_db_dbi.Error: - return False - try: - cursor = connection.cursor() - try: - cursor.execute("SELECT 1 FROM SYSIBM.SYSDUMMY1") - row = cursor.fetchone() - finally: - cursor.close() - return row is not None and row[0] == 1 - except ibm_db_dbi.Error: - return False - finally: - connection.close() - - -@dataclass -class Db2Service(ServiceContainer): - """Container representation for IBM Db2 Docker service.""" - - user: str - password: str - database: str - - -def _find_running_container(client: "DockerClient", name: str) -> "Container | None": - """Return the running container with exactly this name, if any.""" - for container in client.containers.list(filters={"name": name}): - if container.name == name: - return container - return None - - -@contextlib.contextmanager -def _run_privileged_container( - image: str, - name: str, - env: "dict[str, str]", - container_port: int = DB2_CONTAINER_PORT, - timeout: int = DB2_READY_TIMEOUT, -) -> "Generator[ServiceContainer, None, None]": - """Run a privileged container, reusing a running container with the same name. - - Container creation is serialized across processes with a file lock. The container is stopped - when the context exits. - - Args: - image: Image reference. - name: Container name suffix; the container is named ``pytest_databases_``. - env: Container environment. - container_port: Container port to publish on a random host port. - timeout: Seconds to wait for Docker to report the port mapping. - - Yields: - The container with its published host and port. - - Raises: - RuntimeError: If Docker never reports the published port. - """ - container_name = f"pytest_databases_{name}" - try: - client = get_docker_client() - except Exception as exc: - pytest.skip(f"Docker client is unavailable: {exc}") - try: - with filelock.FileLock(Path(tempfile.gettempdir()) / f"{container_name}.lock"): - try: - container = _find_running_container(client, container_name) - except Exception as exc: - pytest.skip(f"Docker container check failed: {exc}") - if container is None: - try: - container = client.containers.run( - image, - detach=True, - remove=True, - privileged=True, - ports={f"{container_port}/tcp": None}, - environment=env, - labels=["pytest_databases"], - name=container_name, - ) - except Exception as exc: - pytest.skip(f"Failed to run Db2 container: {exc}") - binding_key = f"{container_port}/tcp" - deadline = time.monotonic() + timeout - while True: - try: - container.reload() - except Exception as exc: - pytest.skip(f"Db2 container reload failed: {exc}") - bindings = container.ports.get(binding_key) - if bindings: - break - if time.monotonic() >= deadline: - pytest.skip(f"Service {container_name!r} never published port {binding_key}") - time.sleep(0.5) - try: - yield ServiceContainer(container=container, host="127.0.0.1", port=int(bindings[0]["HostPort"])) - finally: - with contextlib.suppress(NotFound): - container.stop() - finally: - client.close() - - -def _wait_for_db2(service: Db2Service, name: str, timeout: int = DB2_READY_TIMEOUT) -> None: - """Block until Db2 answers a query. - - Args: - service: Service to probe. - name: Service name used in error messages. - timeout: Seconds to wait. - """ - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if db2_responsive(service.host, service.port, service.database, service.user, service.password): - return - try: - service.container.reload() - except NotFound: - pytest.skip(f"Service {name!r} failed to come online: the container exited") - except Exception as exc: - pytest.skip(f"Service {name!r} failed to reload: {exc}") - if service.container.status != "running": - pytest.skip(f"Service {name!r} failed to come online: the container is {service.container.status}") - time.sleep(DB2_READY_INTERVAL) - pytest.skip(f"Service {name!r} failed to come online within {timeout} seconds") - - -@contextlib.contextmanager -def _provide_db2_service( - image: str, name: str, database: str, user: str, password: str -) -> "Generator[Db2Service, None, None]": - """Run the Db2 Community container and wait until the database answers a query. - - Args: - image: Db2 Community image reference. - name: Container name suffix; xdist workers append their worker number. - database: Database created on first boot. - user: Instance owner. - password: Instance owner password. - - Yields: - The ready Db2 service. - """ - worker_num = get_xdist_worker_num() - if worker_num is not None: - name = f"{name}_{worker_num}" - - env = { - "LICENSE": "accept", - "DB2INSTANCE": user, - "DB2INST1_PASSWORD": password, - "DBNAME": database, - "BLU": "false", - "ENABLE_ORACLE_COMPATIBILITY": "false", - "AUTOCONFIG": "false", - "ARCHIVE_LOGS": "false", - } - with _run_privileged_container(image=image, name=name, env=env) as container: - service = Db2Service( - container=container.container, - host=container.host, - port=container.port, - user=user, - password=password, - database=database, - ) - _wait_for_db2(service, name) - yield service - - -@pytest.fixture(autouse=False, scope="session") -def db2_image() -> str: - """Docker image for IBM Db2 Community Edition.""" - return "icr.io/db2_community/db2:11.5.9.0" - - -@pytest.fixture(autouse=False, scope="session") -def db2_database() -> str: - """Default database name for Db2 container.""" - return "testdb" - - -@pytest.fixture(autouse=False, scope="session") -def db2_user() -> str: - """Default instance owner and user for Db2 container.""" - return "db2inst1" - - -@pytest.fixture(autouse=False, scope="session") -def db2_password() -> str: - """Default password for Db2 container user.""" - return "password" - - -@pytest.fixture(autouse=False, scope="session") -def db2_service( - docker_service: "DockerService", db2_image: str, db2_database: str, db2_user: str, db2_password: str -) -> "Generator[Db2Service, None, None]": - """Session-scoped Db2 service running in a privileged local container. - - Depends on ``docker_service`` so pytest-databases initializes Docker before the Db2 container - starts. - """ - try: - with _provide_db2_service( - image=db2_image, name="db2_test", database=db2_database, user=db2_user, password=db2_password - ) as service: - yield service - except Exception as exc: - pytest.skip(f"Db2 container service unavailable: {exc}") - - -@pytest.fixture(autouse=False, scope="session") -def db2_connection_config(db2_service: Db2Service) -> "dict[str, Any]": - """Connection parameters for the Db2 service.""" - return { - "database": db2_service.database, - "hostname": db2_service.host, - "port": db2_service.port, - "user": db2_service.user, - "password": db2_service.password, - } - - -@pytest.fixture(autouse=False, scope="session") -def db2_sync_config(db2_connection_config: "dict[str, Any]") -> "Generator[Db2SyncConfig, None, None]": - """Session-scoped Db2SyncConfig for the Db2 service.""" - config = Db2SyncConfig(connection_config=db2_connection_config) - try: - yield config - finally: - config.close_pool() - - -@pytest.fixture(autouse=False, scope="session") -async def db2_async_config(db2_connection_config: "dict[str, Any]") -> "AsyncGenerator[Db2AsyncConfig, None]": - """Session-scoped Db2AsyncConfig for the Db2 service.""" - config = Db2AsyncConfig(connection_config=db2_connection_config) - try: - yield config - finally: - await config.close_pool() - - -@pytest.fixture(autouse=False, scope="function") -def db2_session(db2_sync_config: Db2SyncConfig) -> "Generator[Db2SyncDriver, None, None]": - """Function-scoped Db2 sync driver session.""" - with db2_sync_config.provide_session() as driver: - yield driver - - -@pytest.fixture(autouse=False, scope="function") -async def db2_async_session(db2_async_config: Db2AsyncConfig) -> "AsyncGenerator[Db2AsyncDriver, None]": - """Function-scoped Db2 async driver session.""" - async with db2_async_config.provide_session() as driver: - yield driver diff --git a/tests/unit/test_import_time.py b/tests/unit/test_import_time.py index 4e562be23..cf01e38a1 100644 --- a/tests/unit/test_import_time.py +++ b/tests/unit/test_import_time.py @@ -1,12 +1,15 @@ """Regression tests for import-time optional dependency leakage.""" import ast +import importlib import subprocess import sys from pathlib import Path import pytest +from tests.conftest import should_run_optional_cloud_suite + FORBIDDEN = ( "pandas", "polars", @@ -37,6 +40,7 @@ ], ) def test_first_use_avoids_unrelated_imports(statement: str, unused: tuple[str, ...]) -> None: + """First-use imports of focused submodules avoid loading unrelated subsystems.""" script = f"import sys\n{statement}\nassert not set({unused!r}).intersection(sys.modules)" subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, check=True) @@ -94,12 +98,30 @@ def test_import_sqlspec_does_not_import_heavy_optional_deps() -> None: @pytest.mark.skipif(any(name in sys.modules for name in FORBIDDEN), reason="forbidden deps already preloaded") def test_import_sqlspec_does_not_import_heavy_optional_deps_in_process() -> None: """Same-process import should also stay clean when the environment is pristine.""" - import sqlspec # noqa: F401 + importlib.import_module("sqlspec") leaked = [name for name in FORBIDDEN if name in sys.modules] assert not leaked, f"import sqlspec eagerly imported: {leaked}" +def test_unit_pytest_session_does_not_load_docker_or_integration_plugins() -> None: + """Collecting unit tests does not load Docker service providers or integration fixtures.""" + script = """ +import sys +import pytest + +exit_code = pytest.main(["--collect-only", "-q", "-p", "no:cacheprovider", "tests/unit/test_import_time.py"]) +assert exit_code == 0, f"pytest collection failed with exit code {exit_code}" +leaked = [ + name + for name in sorted(sys.modules) + if name.startswith(("pytest_databases.docker", "tests.integration", "tests.plugins")) +] +assert leaked == [], f"unit collection loaded integration plugins: {leaked}" +""" + subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, check=True) + + def test_adbc_import_does_not_require_or_load_duckdb() -> None: """Importing ADBC modules must not require or import DuckDB.""" script = """ @@ -167,3 +189,22 @@ def test_no_unintended_cross_adapter_imports() -> None: ) assert not violations, "Found cross-adapter imports:\n" + "\n".join(violations) + + +def test_cloud_emulator_opt_in_accepts_cli_flag_or_env_var(monkeypatch: pytest.MonkeyPatch) -> None: + """Cloud emulator opt-in runs when CI is active or either CLI flag or env var is enabled.""" + monkeypatch.delenv("CI", raising=False) + monkeypatch.delenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", raising=False) + + assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is False + assert should_run_optional_cloud_suite(True, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True + + monkeypatch.setenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", "1") + assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True + + monkeypatch.setenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", "true") + assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True + + monkeypatch.delenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", raising=False) + monkeypatch.setenv("CI", "true") + assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True diff --git a/uv.lock b/uv.lock index b323f7d4b..268e81a6c 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,8 @@ resolution-markers = [ ] [options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" [options.exclude-newer-package] advanced-alchemy = false @@ -154,7 +156,7 @@ wheels = [ [[package]] name = "aiobotocore" -version = "3.10.0" +version = "3.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -165,9 +167,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/40/659f2ac3915d329e3e8e5bf674d35ce1774f3117c6f60339d5e05e505c36/aiobotocore-3.10.0.tar.gz", hash = "sha256:35480ef11c51e23087273b85d97f80da2f356a46ce5f3fb88e24483cd343ffdb", size = 530781, upload-time = "2026-10-23T10:44:50.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/95/4f7e0453d5e81bdb1b40ecf09f135bb88e12ce27fdb3efa125022dcca5d7/aiobotocore-3.9.2.tar.gz", hash = "sha256:8e32238c5bd77717ab1ac90077ff51ab141d025528115ccc7cc9b50b2d176e5f", size = 524343, upload-time = "2026-10-01T02:01:59.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/bc/3bb8965204ba51bbf4d021bc89d1357cd2179a17ef71b5fc429e21b511a2/aiobotocore-3.10.0-py3-none-any.whl", hash = "sha256:de0ccbcb82b6b0009f1b6667f48442e9d0432f9b8bfe6172fa51777cbea89d03", size = 102759, upload-time = "2026-10-23T10:44:49.109Z" }, + { url = "https://files.pythonhosted.org/packages/09/ab/c8f982c4dfaab49200e31820a4ffe6275356f7dcaaa1024735d8f8b4e048/aiobotocore-3.9.2-py3-none-any.whl", hash = "sha256:363b4892423b272eb84afbb58000da0be6ccd0cf8a68b98438c957d05b841efc", size = 102571, upload-time = "2026-10-01T02:01:57.883Z" }, ] [[package]] @@ -391,6 +393,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.5" @@ -424,19 +435,19 @@ wheels = [ [[package]] name = "arrow-odbc" -version = "10.6.1" +version = "10.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "pyarrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/f8/7d0d0b88cadd9d28050ad7bce851af086857eb1401d09b54b26045bfbe99/arrow_odbc-10.6.1.tar.gz", hash = "sha256:f7449d292632f455f72e7ad8bb6b4fe386ac5602738225526294e1ffee0374d5", size = 107639, upload-time = "2026-10-19T20:22:59.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/2e/621da34d93b50666b0b096e3a61dd2118a31778181eb693955cd57c93e4d/arrow_odbc-10.6.0.tar.gz", hash = "sha256:02ab4dd902bb42dd37a104753f4224b745d4a4f437fab7a6d3182865c4f70c2e", size = 106916, upload-time = "2026-10-07T12:29:54.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/17/8c32bd7afa88b6cb8ee120e0051218d2bab52dd7e1e60387230ac5b00ef1/arrow_odbc-10.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4de744a4f20ebe574030bb1943f9aa10cb242b77ac7128a1efd1539d943cee6e", size = 753105, upload-time = "2026-10-19T20:25:14.844Z" }, - { url = "https://files.pythonhosted.org/packages/88/7f/e7c884fe16844f38899053f0e2e9f5ad66a984bb79823b1aef6d17b743d5/arrow_odbc-10.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eafdf71851aac18c37822a67b57d704f2b9a7b133205da253445d374ca6c90c2", size = 735430, upload-time = "2026-10-19T20:22:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/67/54/0deab13f42cbdd8aaeec9b957164b49b54d805fb3c94ca27d3857a07ee09/arrow_odbc-10.6.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:72d97c33cc41bde4b071bad8b20932e360d11f4dad562bc0687cd59b8a11a3cc", size = 843468, upload-time = "2026-10-19T20:28:59.043Z" }, - { url = "https://files.pythonhosted.org/packages/94/bb/920f8e03a0340b8c8ad945c6fde73df07519596def2e40ff6ef70358ec8f/arrow_odbc-10.6.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:78d48b9fe0291b32f50e8b1fb81c5bb2cd74fe7f636b34052551070a51ae2a5a", size = 869855, upload-time = "2026-10-19T20:25:20.298Z" }, - { url = "https://files.pythonhosted.org/packages/ec/12/311cd3f26939ead9b7f276988b9bd9d7b049c47c84af6b71cd35bd3d24c0/arrow_odbc-10.6.1-py3-none-win_amd64.whl", hash = "sha256:6226df0d91af94bdb47f575933e6c704e46d525d8b37348c43f7207f4b7d5e9b", size = 654895, upload-time = "2026-10-19T20:25:03.548Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f7/d2f80aba2eafcf69eae6bee99cc171e663817041060ff70fd85c23cda354/arrow_odbc-10.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:94aba247e2300d4afdcfd8957c44fa8c64bdde4831ea4c91bb67038aeb3886af", size = 796971, upload-time = "2026-10-07T12:32:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f5/5022c69e7aff7524f0f391cef8e9c23e18eafbc27058940bc5447c460888/arrow_odbc-10.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6726932e3790b6448aea82df258eb1cf2d8e6c34045d15554da523ea3521d2e5", size = 773014, upload-time = "2026-10-07T12:29:53.261Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/3833166b4d19a75025b9cd4aee5b7d31eaf98f6cd460bcd04e6f0e658960/arrow_odbc-10.6.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1d8eedba30458ef1e5c22831d4d37df27b2bceddeff1ea106a51c54f1adcdae0", size = 882410, upload-time = "2026-10-07T12:28:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c1/72a3d03d66befec63b490d1533c9a2f3f4b7a12ef16cb99c8cd672d5211e/arrow_odbc-10.6.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:037f304b85ef825a16c01a0e53850be7ba2791f1f1085e6f80e0b67a38d30512", size = 914702, upload-time = "2026-10-07T12:29:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/86/13/591a07f4a77fa9ac6f6458905466ba5c80fd539eb8e1454bc9d8f0c9515d/arrow_odbc-10.6.0-py3-none-win_amd64.whl", hash = "sha256:9d05d7e8ed3f4af62d33790e3fbec190a0ead112ab898ddd3566843ae09954ff", size = 693289, upload-time = "2026-10-07T12:29:32.481Z" }, ] [[package]] @@ -505,66 +516,58 @@ wheels = [ [[package]] name = "asyncmy" -version = "0.2.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/f9/9ca78e8191897b1b42eaecb2585c3d88b67662a020146d6b0c908cff0082/asyncmy-0.2.17.tar.gz", hash = "sha256:a6b615e1062a1ecb943b75eb30dc45c54a9aaa618d85010f17822c77921e8701", size = 94746, upload-time = "2026-10-22T02:57:45.69Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/54/6c598fd913c33783996232b601875032421fa320f873a3e892cdf9c4e778/asyncmy-0.2.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f739450c5d107b19427c3e0688116019daff3695d57a24dec903edfc8e2d7b8", size = 2265759, upload-time = "2026-10-22T02:56:15.649Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/823d21ae673577475154b10f1fbde70d4c025075e4d044bf6da602984004/asyncmy-0.2.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efe8814f146fba5b9a05e63d87a17f484d0508b2290ade37a533bbcfb134bfe", size = 2228400, upload-time = "2026-10-22T02:56:16.776Z" }, - { url = "https://files.pythonhosted.org/packages/37/5d/cbefa4e5031498d7bd4a36078af1a9a39cfc47bde18a5d06046918b1ea16/asyncmy-0.2.17-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b250f2222e857fa26464826b7c84746a095184cfe0ef1da5fcefa2e65f52f816", size = 6907415, upload-time = "2026-10-22T02:56:18.003Z" }, - { url = "https://files.pythonhosted.org/packages/98/ef/213fbf8fd41da577d215c8ee1c5d5335ed2f17361f5a56a330efdacfc201/asyncmy-0.2.17-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:598ec32aa6dd186a6952b763f17fbb7ceb0fa394f008a85f49746c16229a86e3", size = 6939190, upload-time = "2026-10-22T02:56:19.593Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/68c7fbd531f065a5967ef0fbb3e845e9fe275506dbefdfb57e03d7d4cdf9/asyncmy-0.2.17-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe830a567d823d2219a4832d27a39298cc3844e0292acb88de18f57cc42e7c46", size = 6687418, upload-time = "2026-10-22T02:56:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/0b02217614867fab9428db058977820007eaafb12578972c7eda5f5869c2/asyncmy-0.2.17-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:898201715eddebf26b335156e1cb024bac83ff2331f25debdf75ba715cec205b", size = 6778721, upload-time = "2026-10-22T02:56:22.235Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/1071a3b8ccb822bbfc62cf3b5d9e1dc77362b99222516ae539620ba64b20/asyncmy-0.2.17-cp311-cp311-win32.whl", hash = "sha256:43fa09113289131a4c39734da042cc8095394e90f6231074482918442d4d1ac4", size = 2013751, upload-time = "2026-10-22T02:56:23.708Z" }, - { url = "https://files.pythonhosted.org/packages/8e/41/b5399cd780d4100385ae260401e803f2e740ce41065f1c3f2ad7f7c9dac3/asyncmy-0.2.17-cp311-cp311-win_amd64.whl", hash = "sha256:9826f6efee0c882288833b79851672825eec64cd321a0eea4e599009cd154cef", size = 2115136, upload-time = "2026-10-22T02:56:25.125Z" }, - { url = "https://files.pythonhosted.org/packages/81/d7/f4e1e37835314a69efa0233f99271a60c15723032bdbe4e5c860bee23369/asyncmy-0.2.17-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:749577dd5dd511ffb7bb8ca2f25e122276349ae78902ebf5090e745f4ff2bb55", size = 2235989, upload-time = "2026-10-22T02:56:26.311Z" }, - { url = "https://files.pythonhosted.org/packages/70/09/ec8af1092ebed7c92a18817553b7142369d85bfc229496d4178262bb8a75/asyncmy-0.2.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:835d3d02edc913ae94dc64d0e78419a4eeda14f73d75e74242eea5616880673f", size = 2215233, upload-time = "2026-10-22T02:56:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ea/2b821755b27ce38a2fbb8bc48409cc33def23861023c131f45af1501e31b/asyncmy-0.2.17-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7a4a01b42b087232953542527961d5d6a1795b37a791c9ae33d89ac24d704c", size = 6791794, upload-time = "2026-10-22T02:56:28.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/4a/77936f41d3e4d3584a37f0e63779023f2c0fe31550cc77609f6651fc71a0/asyncmy-0.2.17-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eb0ba89f38f10cabbada24e37c2a0d25c6612210396c2a5a01029dce8110a3a", size = 6858124, upload-time = "2026-10-22T02:56:29.98Z" }, - { url = "https://files.pythonhosted.org/packages/17/ba/b762efcd343b5610a28f00ef90c7fb813f82016b75cd8d6eeb13057ba800/asyncmy-0.2.17-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e19501db949d142ce9281553282df6408a441156bc0a523320e22f0386a7a972", size = 6509117, upload-time = "2026-10-22T02:56:31.527Z" }, - { url = "https://files.pythonhosted.org/packages/ee/92/88be111f0e8600880c876407445766b774552e4fba70205b543d4be87c3e/asyncmy-0.2.17-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19ed009035fddb33d545fb2ab2acff78cc54ad8593de885eeb2a9b8caf73b1dd", size = 6634912, upload-time = "2026-10-22T02:56:33.033Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b5/676cd36e17149cca9c51572182ecadd42dbc62f4123dd69993e4fa209f8a/asyncmy-0.2.17-cp312-cp312-win32.whl", hash = "sha256:d6c1567b8e49ec2fede53ff62d670f5a6353efd332525a1f4f165dfc7607b3cd", size = 1993648, upload-time = "2026-10-22T02:56:34.805Z" }, - { url = "https://files.pythonhosted.org/packages/43/94/bbd6bcc408977fd445a05499dd9700f3a7366f793dc585330f6fc27c2b86/asyncmy-0.2.17-cp312-cp312-win_amd64.whl", hash = "sha256:b788da2366a0d855ab58619fe9b88f69410e23f191094665703f3dcad1cc14a5", size = 2095550, upload-time = "2026-10-22T02:56:35.859Z" }, - { url = "https://files.pythonhosted.org/packages/fa/79/fe1113b37245f25eef4af688ce3770f8336f53261c10f27ba71d5e823d3f/asyncmy-0.2.17-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9183af6cb0a85f593b877afbbbec6981b1abd4fec0cc511d55c496fbf3fb5a9b", size = 2238479, upload-time = "2026-10-22T02:56:36.962Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/244b4ff95bb60ced6ec28544530dedd786834abf3a0e2667ea878524ec13/asyncmy-0.2.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b32d559b6aadcb16f57a32f8977f857033fb5082e2c89f10bf234d93225bf58", size = 2214565, upload-time = "2026-10-22T02:56:38.316Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/bc51846e9048d3ece9e021ca8c8dac046e118ecb321296fae7c888751759/asyncmy-0.2.17-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:734451670cff1a79069a051df49d25d4493e61d7e533ec6a906e2d083fde037a", size = 6732801, upload-time = "2026-10-22T02:56:39.59Z" }, - { url = "https://files.pythonhosted.org/packages/3b/60/ea19f1a75c08830db445ebe5478c5376f67d1e2bec68b5607a8dd49c6164/asyncmy-0.2.17-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4307e9b1090bb2152e1e0dafc50733302278bafb0f66310ad32356ebaba7eb33", size = 6796055, upload-time = "2026-10-22T02:56:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b8/77d5b234ae8398243e8faac4c512d73902ad566350f95472e910e7002527/asyncmy-0.2.17-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e8813ab22e007cc136a156ff5fdb990a957f97e6c9f7fe3a6582d369f6b53e4", size = 6460031, upload-time = "2026-10-22T02:56:42.211Z" }, - { url = "https://files.pythonhosted.org/packages/a1/65/63e1c4ae1f620cced28bc3628b2790dbf8987e268e3c3d0166449735ae68/asyncmy-0.2.17-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d761b7f2afb3198da404c60cd6f2bfd0744bc5fbc1d947d77807bb696b70c22a", size = 6592706, upload-time = "2026-10-22T02:56:43.916Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/bc94208464d723a0e3a57357e0b739386ff58a436c24e0bb2f74d7c1a653/asyncmy-0.2.17-cp313-cp313-win32.whl", hash = "sha256:30b111510937208ac22324d072ee0a35a7da69b3068918597bcc4358609133e6", size = 1991419, upload-time = "2026-10-22T02:56:45.128Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8c/47f164e15e7ca8223c7a5c7426e5cb7fdf0eefe8eb0718e398e8544e23ea/asyncmy-0.2.17-cp313-cp313-win_amd64.whl", hash = "sha256:9c7a1eea12b76e98fe5c96221d5a70d7e2c281cd839036462a7f372b37c5030d", size = 2091186, upload-time = "2026-10-22T02:56:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/21/65/484810632ef1466dd29614b6d09a9848017ff89893362820002acbb2f5fe/asyncmy-0.2.17-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:243b57105d97f19103d512ba22ec40648a6d2ae7140e0f307dff6769e8f4827d", size = 2245293, upload-time = "2026-10-22T02:56:47.606Z" }, - { url = "https://files.pythonhosted.org/packages/15/dd/f9fe4c883efd3e762bc0a3fde9697df3cdf0c542404487e3136802a1fa2f/asyncmy-0.2.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:876393715478830d0255be289227f0d2ab2b069df7c6f56b83f9d133146624e6", size = 2227552, upload-time = "2026-10-22T02:56:48.783Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/b7805690d22b955eae402a14fcad34783f65123e76d4fd4ce884bbe2b579/asyncmy-0.2.17-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d49bf017572f5d4bc91440be3d9ed66ae091d6c8393839618f7e8d7c63581c30", size = 6729426, upload-time = "2026-10-22T02:56:50.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ef/7283a8173b481bb3bf1641b680414aa9d55b001af22416b0240eb7d2ee18/asyncmy-0.2.17-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c44635e83e2d755c06affd4373ca51cc04f5ecf8a91e30b86bc9eebb9d1b0ef0", size = 6742737, upload-time = "2026-10-22T02:56:51.722Z" }, - { url = "https://files.pythonhosted.org/packages/c5/7b/99bc04e3ba4f892d80db4595a086ba848f0104d2d5ebe36f181dea330cf1/asyncmy-0.2.17-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:542f7652d8b27f989c646ec8ba0d67bd9620afff41038ba3668fb0c18bd0599b", size = 6472591, upload-time = "2026-10-22T02:56:53.418Z" }, - { url = "https://files.pythonhosted.org/packages/de/c0/12eae0dbab2ece26c2da10f4e66df5fc13a94732281e7a3535fd1cdd17c5/asyncmy-0.2.17-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:11c5518baaa3e3b79689cc089ce3f07ab2950769191d657c03a8babfb15f61bd", size = 6530572, upload-time = "2026-10-22T02:56:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/ed/72/389a0a60ef487b7df23e85937c060206c39bc2cf6cdbaf990e1197ce5a2a/asyncmy-0.2.17-cp314-cp314-win32.whl", hash = "sha256:6add4c67840dd3f394db250b98f3a0766e600912012fd38767fa4ce93bd5b5ab", size = 1986841, upload-time = "2026-10-22T02:56:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c2/3a13c161b4bd1ee6f7eb193efa1b5b9a1f572281e397f1e1008de963788e/asyncmy-0.2.17-cp314-cp314-win_amd64.whl", hash = "sha256:b212f20323f5eb0604f645c0f4a705f6db36b94c3f2a842fc579bac95577bc76", size = 2090727, upload-time = "2026-10-22T02:56:57.812Z" }, - { url = "https://files.pythonhosted.org/packages/57/3d/2fd032a4c666d8a52656366fef317a848ab9af1f1c6be68d30eea65d39e7/asyncmy-0.2.17-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:95610b4cd2f73b47cc0ad39d89f3f96c73f60505b8b08b58d91978f48bb5e94a", size = 3076795, upload-time = "2026-10-22T02:56:58.864Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/39e622c8d9d541df0740f7a77b2808724e11186f16126ce765e9e8746944/asyncmy-0.2.17-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:264720d5be4948e4661b7cc05f6a6e42dabbd3ffee422d49dc586a78c44be677", size = 3053252, upload-time = "2026-10-22T02:57:00.129Z" }, - { url = "https://files.pythonhosted.org/packages/39/9e/f09dd8940562deb3930ea3fb3c45c04ff090c4a6e556c2c1fe7ef175beea/asyncmy-0.2.17-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4d952a8f5879964b4544b883540ffc12d77f2d21c491bdff833ad11427dba0a", size = 12314597, upload-time = "2026-10-22T02:57:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/9c/3d/dd9081c9c619c73a7e7def9f1051af2dfc5cc2ae1de0c8ea0c8ce4db4e27/asyncmy-0.2.17-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37d86799aea0c4b34a9acf96f1fd3d185fe408899dbba8d22b072d0577a68f3", size = 11970826, upload-time = "2026-10-22T02:57:03.282Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/d0cd968e392aa04ddbe7d2c2a7643aba921f3f0d6f95bb3923e71e5aa497/asyncmy-0.2.17-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a158eff4a9b88d8336f2a802b1400cfc71bc38bceff1361a56f4ac6d1531827f", size = 11674439, upload-time = "2026-10-22T02:57:05.167Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b1/07319f7be182687990ab83c9274f967983db306502a3869f7d34bd1b416f/asyncmy-0.2.17-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9caa1e6c3514e60f35f4ac91bc2631fe77adb52e52de305d3158209b1ba8b3d", size = 11580994, upload-time = "2026-10-22T02:57:07.044Z" }, - { url = "https://files.pythonhosted.org/packages/48/1f/0e6b6bb9650b2754e4e71389715471e71814f37c586704b0a07d1722bde0/asyncmy-0.2.17-cp314-cp314t-win32.whl", hash = "sha256:0215d7e577c5451ba5061e558cbdaf1a3f3e63589d530294b8aa25636ee96cc5", size = 2572323, upload-time = "2026-10-22T02:57:08.988Z" }, - { url = "https://files.pythonhosted.org/packages/af/b7/438a021fabe31f372c1beb11224a730454bc81eee4413787a531fb5ff320/asyncmy-0.2.17-cp314-cp314t-win_amd64.whl", hash = "sha256:57f5f907f613bc1f2c49bb3ef07b07c0e0dfbc4427556abafef051d4f189fd55", size = 2780927, upload-time = "2026-10-22T02:57:10.107Z" }, - { url = "https://files.pythonhosted.org/packages/58/fb/f1eb227d37b7470b3b4bb09c51ef1fab606886e5c4b955af4bc94d648075/asyncmy-0.2.17-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a5e073c5bab0f65d555b599f264a241ba7dd0a1ff6a098eb1d409ec876f226f2", size = 2245716, upload-time = "2026-10-22T02:57:11.712Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/c684dde3aa00474e9fea71ddfa4991eb472168bcdeb17d3c35048240c4e8/asyncmy-0.2.17-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0d564e78cbfce291b1b105d15a5401053f8133227b1aac630b4217870620749c", size = 2226027, upload-time = "2026-10-22T02:57:12.835Z" }, - { url = "https://files.pythonhosted.org/packages/19/5a/de500352d4fa9fa6236131e4fd1822c5121294a4dbecff0d3dfd8bf4dc47/asyncmy-0.2.17-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a763ad306eb7b7fe2ff1302100ca2215281b17850fcac9e64123206b4945188b", size = 6724746, upload-time = "2026-10-22T02:57:14.708Z" }, - { url = "https://files.pythonhosted.org/packages/72/14/3b7ea816d6b48997a0a6b7fae6aee41e7fcac7c84916600eb0a85ff1c267/asyncmy-0.2.17-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31879dd05d767b743e0f295735cb6470f92a84e42a3766cc7101fc97db1e6101", size = 6771063, upload-time = "2026-10-22T02:57:16.137Z" }, - { url = "https://files.pythonhosted.org/packages/05/7c/d76bdd36e21a9bfa5dd65eca7cdfb9815ff9ad68075fdaf35067104ef7e3/asyncmy-0.2.17-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ed7a3b8dfb10f2ff6707f2b14d30ad0dcc1a84a24333893bd7739c17c83f4ca3", size = 6469593, upload-time = "2026-10-22T02:57:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/f3/0a/5c12442264cafe934bee63a9734f27a8f814f53771a5ffee25c07306d0b1/asyncmy-0.2.17-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9951457ac149716c0780a043802e93e12f7f775b083a0c6c9332e84525c10048", size = 6545273, upload-time = "2026-10-22T02:57:19.151Z" }, - { url = "https://files.pythonhosted.org/packages/ee/66/3935e2de234bd17aa15ec2765ac99b25aded02b72f1941462cd62b5d3e80/asyncmy-0.2.17-cp315-cp315-win32.whl", hash = "sha256:33c0e5f404ea2d415407204051da243e3ae421ec9ab9d7f336bd57b381128714", size = 1985793, upload-time = "2026-10-22T02:57:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/78/0b/878988d0803ac8d7694581a3597cb90afaa5d52abb8346eba740a2664638/asyncmy-0.2.17-cp315-cp315-win_amd64.whl", hash = "sha256:1f977ba4d152d7268234eb61cff79c1a94a83af793fc5584d7b6f3967ea78d2b", size = 2089383, upload-time = "2026-10-22T02:57:21.637Z" }, - { url = "https://files.pythonhosted.org/packages/46/15/e6781b55d010f8d32c7c4dfcc1dd98636850c8c2e471ac2c209e99246460/asyncmy-0.2.17-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0a003bde5acc50c9f1eb050a3bd2ed183e66c1ff61e9897cdab749c80363fb0c", size = 3072447, upload-time = "2026-10-22T02:57:22.943Z" }, - { url = "https://files.pythonhosted.org/packages/1f/9c/e3ca4eba5aa2a78627d8bea7b3501241af4e6b32793be15848bd1ca4a400/asyncmy-0.2.17-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:62a9f130533f087beae03810f8865af4d425cda792aa190f03687ac1235f2459", size = 3047069, upload-time = "2026-10-22T02:57:24.345Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b2/6b1b94c29afbf21b2301bd1996d807d6c472c98345b8e3778946ebc260d6/asyncmy-0.2.17-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d906092f2686fd55caed1e9e8ed267f1b9562d15b6bd4e07d62826a5b4b07d9", size = 12293960, upload-time = "2026-10-22T02:57:25.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/bb/d6a21de5ca76c24e2c339423e9d3cae797db8f8bb0b9d9aab740e927efd6/asyncmy-0.2.17-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab098978004f34087dcd9949853619af58791f55731cc550d3e9af62c5b4063", size = 12208949, upload-time = "2026-10-22T02:57:27.659Z" }, - { url = "https://files.pythonhosted.org/packages/09/7e/f619042a40f5a34f46bb7b47b45289fcff8a23ae392a159b239862149ffa/asyncmy-0.2.17-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:505fbc81e136bd2805054887bd1ef537789f5e741d1bfd12081da0999097d563", size = 11661107, upload-time = "2026-10-22T02:57:29.589Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f8/7196da01e59d1b6ea16378a1d40692fe2f9e1670642dc0320b26f33099d3/asyncmy-0.2.17-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dd3d46af1bddc9601aac751fa39df7fe3f869a47f4fe7037b066fe9a66934844", size = 11745424, upload-time = "2026-10-22T02:57:31.318Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2b/fc65878ee5dd9257a9ee05b30fbb746cbc951c63a5c9c9969f9b0c008d7b/asyncmy-0.2.17-cp315-cp315t-win32.whl", hash = "sha256:544ce6607a52beffb19cfcf90ed3b005af822db7e0a91cf57a6b78802f2c7005", size = 2568999, upload-time = "2026-10-22T02:57:33.138Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/726aa0a8ade15cd51bdd04daa37fb64f9afbb1064ec43112355b8366bc82/asyncmy-0.2.17-cp315-cp315t-win_amd64.whl", hash = "sha256:74bc479969db2900f46405120bb5735fe183ab78960a6e3c6c05a1ac41505daa", size = 2773819, upload-time = "2026-10-22T02:57:34.311Z" }, +version = "0.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/a2/cf891f7c05b6292e0966c3870332d7778c14de912b33db4a895ac5151b9e/asyncmy-0.2.16.tar.gz", hash = "sha256:92a9c5d1ddb143783360b92f8abdc72612d7a2b2efb2a07482d2a816c9223be8", size = 94368, upload-time = "2026-10-06T10:52:58.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/f4/880a3392c756cf488ee60b656e57a8fee50252e469daf9d061a4d30a82dd/asyncmy-0.2.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd2016f01d67b4d8fe8ec04e2705c93740db3c6d111bdf4a15630116e2c6fa20", size = 2263246, upload-time = "2026-10-06T10:51:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/4313af9b1401f8c418a4f4cceea75467551e119ea52e6ef9bb8aa0cc8e46/asyncmy-0.2.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b36f27c18a349928242ecdcae101ef4ff130897038b7e7e6a6677f42a396129c", size = 2225909, upload-time = "2026-10-06T10:51:37.208Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2d/b28c7cd0a774c8e8f88466b9ab5992c932971bb4ee9923bf88cdff5c1d7d/asyncmy-0.2.16-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2feec5a05ea43eab2b9f3419208dfeace182d9a2291e0cb2a8a60e6284d72", size = 6899414, upload-time = "2026-10-06T10:51:38.422Z" }, + { url = "https://files.pythonhosted.org/packages/3a/60/0c33f36f1fcbf60a18adc31c993c6655c22de901f89a2c0dfe11076a5755/asyncmy-0.2.16-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e658bd49d94f322ebd36f7e687cc88972ec667b7b6f8dda29a78fb8da675123c", size = 6933200, upload-time = "2026-10-06T10:51:39.829Z" }, + { url = "https://files.pythonhosted.org/packages/24/86/1da36a00a1fe1faca1fe109fe878e9b8087f0c828af4e22b7861d31faad8/asyncmy-0.2.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b46824fea69b1cc6d94c15adbe351ecbfb2fa663ea50d61c6ca618f4bf92f03f", size = 6678147, upload-time = "2026-10-06T10:51:41.201Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2e/206ac3d2d7e08dbc43e78c4accfff54a5cfa7646eafe087e566f49fb945c/asyncmy-0.2.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bd3c8a94a646b0c28e97a599f25c327a9633a3c6738b7a7914869c758560b45f", size = 6771523, upload-time = "2026-10-06T10:51:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/a4d6db6c8429b7d161c77680224bc2bb0efdb8eac83db1367afd281697c3/asyncmy-0.2.16-cp311-cp311-win32.whl", hash = "sha256:ffa76b94895afdcfdd7f6043de2818dda5d5132ccd54a86f94801f163e760999", size = 2011557, upload-time = "2026-10-06T10:51:44.486Z" }, + { url = "https://files.pythonhosted.org/packages/af/70/d87838161b89a07cc4a21883e348e9e8d6eb0e294adc2842ad53a12c6a39/asyncmy-0.2.16-cp311-cp311-win_amd64.whl", hash = "sha256:7ec630f802c861f1300c4a30e30d294a1836f46271b820ff9b6b109588758db6", size = 2112860, upload-time = "2026-10-06T10:51:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/6cc46efe1d4693724ff5e76b50a60a78571efa1439133d0bb78ded8217aa/asyncmy-0.2.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0faad88c3c8fdffe3de6d626f58d2af47fa47531cb6d2100859b8fddd9685847", size = 2233315, upload-time = "2026-10-06T10:51:47.197Z" }, + { url = "https://files.pythonhosted.org/packages/21/72/a8b2e8feafcf3dadd48bd364ddc40d5d2125ffa1d3fd61a0fb715fcb553d/asyncmy-0.2.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20f148342baccae2a7995e745414f999bf116062975b7635bed9557895423681", size = 2212570, upload-time = "2026-10-06T10:51:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/58/73/4fe290478d4898b5c34a46374e9c0604574f503d7d388d853710a4c07305/asyncmy-0.2.16-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f32ef4f8746a2b9073d63950be8a87466426da9bcbc8339943c62b4de34e70a1", size = 6787634, upload-time = "2026-10-06T10:51:49.961Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/ee3052e0b12737e1ea2293ac4b888f69c5a27c3c225a5054ba5e691091fa/asyncmy-0.2.16-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc5b0fba7feec70bfc0a4c571f2e0071e040d052f46447c491f28649a1b70c15", size = 6848958, upload-time = "2026-10-06T10:51:51.522Z" }, + { url = "https://files.pythonhosted.org/packages/76/d4/e1fb370a4dd2f9a295e1189f68afd975c6ad385056e9696e653ca76ffe6a/asyncmy-0.2.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6429983256fc41de0bae3782e2f89ed330b84baa2dfd398a87d9913b27c74620", size = 6505431, upload-time = "2026-10-06T10:51:53.286Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/c1d82f08f482272d06c2572645c0af13a2af2f2309b600ffe98dd2ab8cd8/asyncmy-0.2.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e0acb7aa6cea90f454df9be4fd5e402bea2d30d1d3dab8f70d48031e8627095", size = 6624495, upload-time = "2026-10-06T10:51:54.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/1a/9e0876385c282c308793619a6a05646918904d42270e6229a468f5c77fb8/asyncmy-0.2.16-cp312-cp312-win32.whl", hash = "sha256:c2798f09a62c4dad559951c40f8e89a87ad41758ad19376efe80e9dc0f1ac2d1", size = 1991358, upload-time = "2026-10-06T10:51:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/91/cb/b5d617b87709c17f9de409eb55cbdce4c3c2849d8babe1c54bcc4d413557/asyncmy-0.2.16-cp312-cp312-win_amd64.whl", hash = "sha256:6dd4997a060a2bebe90ac8420e3b6a490b75f5c0a62cafbe7d19acd3f4c2fc9f", size = 2093206, upload-time = "2026-10-06T10:51:57.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ca/8b3d3fd98c68c0c244bafc3560b7869c0db98e46d4befb51001dc51befa8/asyncmy-0.2.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c16a1b3710b98077f1d2cf7fd54387b182a42abb2d49ea9f2dcdb41c46b77ee", size = 2235912, upload-time = "2026-10-06T10:51:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/21/ed/1e28cd1b6915670be596d266913773b8d2c4bac32516446a2d614225fb6d/asyncmy-0.2.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0431d9dafdf3a143674dbc22300d28ee42f82b30948430e870994a1f7d1700ed", size = 2212008, upload-time = "2026-10-06T10:51:59.681Z" }, + { url = "https://files.pythonhosted.org/packages/61/dd/086f85cc2a25e4d010bc0e34da9b4b43f433416b8f804a6fcc2f216bdbc0/asyncmy-0.2.16-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea88549833b99192612d23ce2678cda7cf3bd1c7c548b482d75d7de7be990f7f", size = 6724880, upload-time = "2026-10-06T10:52:01.193Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0c/d80c38f534b88c5cbc8937607b2facd965405bb84f790585ed07ec0a533b/asyncmy-0.2.16-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb9ef0552df7f3857cf58cbea9896fcc0f5db4cfbcc8d98bd89fcf2963f65759", size = 6787251, upload-time = "2026-10-06T10:52:02.478Z" }, + { url = "https://files.pythonhosted.org/packages/fb/42/0ebfc96405b03d77fc6b58930000f832107addec334b4c658b950572f9b7/asyncmy-0.2.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ed8a3073f03cfde57ea401181a97f818cda8eab85470c9d65591664fe9aa42a", size = 6454354, upload-time = "2026-10-06T10:52:04.186Z" }, + { url = "https://files.pythonhosted.org/packages/37/d5/86c165ff1dd47919feb71fdcdfd949edc577a1fb52f71862c7a789e09894/asyncmy-0.2.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8c08c47fd0acfa647a108d065236ff91f6f48cfdf618dfee7ade10dbfba8daf7", size = 6584637, upload-time = "2026-10-06T10:52:05.604Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/aac5a35ecbb4f8c8081c8c91486897a7b719d75aa9cc27b1489dac0cc824/asyncmy-0.2.16-cp313-cp313-win32.whl", hash = "sha256:74ae4c8a001bd041d1bcdbc5a72c63b204806a09327819a354f99c973499ccda", size = 1989029, upload-time = "2026-10-06T10:52:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1c/0187d66ff58855d817616214c5220810f66d5070029773789dc0786af5eb/asyncmy-0.2.16-cp313-cp313-win_amd64.whl", hash = "sha256:091cdff819737e419e7e168d63f3df48d1ec77e196b8275b6b5ac4d19b2cb768", size = 2088793, upload-time = "2026-10-06T10:52:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/55/02/cd8513fc99ce4dc8c25c1c2a1f6d7cb74d64d107f23b3da6e5e5fa6e49e3/asyncmy-0.2.16-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e7fb933dcff03616dc36a7de9cdea85a67a1b2158684af3b5e6e0bd8858bcfdd", size = 3059809, upload-time = "2026-10-06T10:52:09.548Z" }, + { url = "https://files.pythonhosted.org/packages/45/5e/6cc381d7b8921466d1a2049b9a07e6a60420744200ea669c08eafbb1d184/asyncmy-0.2.16-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c79efdc3f6632b80c60900ae9605495a49bd0b81e586e7d837042d5dfd4d1ee1", size = 3028194, upload-time = "2026-10-06T10:52:10.804Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/26bd110fc530d82f6f181f51562bda6574bca302518caf0ac0d050d43cba/asyncmy-0.2.16-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71504dd8d59cb912a84fb54cb3cf5aac094581875b6e53630077dcffad7d282", size = 12288234, upload-time = "2026-10-06T10:52:12.243Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/c14a947c437ee362e655826f5510ae0f42263bfe0deae825cd7943cda55c/asyncmy-0.2.16-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:594cee61496c840611f82c5b6b0607c19aa155442420d16b2c47f2c860a090bc", size = 11996856, upload-time = "2026-10-06T10:52:14.18Z" }, + { url = "https://files.pythonhosted.org/packages/14/f1/f43741a156332428c23e356eed3162015872d01a102f64d523ade3dba383/asyncmy-0.2.16-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80baaa4da31b64b57b0a266656fa4693f1a6c6c0f00ad1dd1e74f76dd9d280cd", size = 11636182, upload-time = "2026-10-06T10:52:16.126Z" }, + { url = "https://files.pythonhosted.org/packages/54/2e/f4158af50e6c38c9a4323c33a9f8f8e16850e7fdd7408a4c9501ef40ff64/asyncmy-0.2.16-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d1677191ba3faf318a7da52cad1f367ccea3301572ab49472e124ab962037f26", size = 11612894, upload-time = "2026-10-06T10:52:18.132Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/4b3d6f18a0e27cbec4fa25b4eab4d5496ef5e6e9c58bf5418aa1e8a2c826/asyncmy-0.2.16-cp313-cp313t-win32.whl", hash = "sha256:f5f9b8484a63261c86322bad878b11a07fd4229b17557bdd72a38fad424b8ffe", size = 2556686, upload-time = "2026-10-06T10:52:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/be/17/e79d2c410c704a11e57bbc037407383c5cbf99b9bbad2733ba862568d7d4/asyncmy-0.2.16-cp313-cp313t-win_amd64.whl", hash = "sha256:9fa9c6d94f8887d89c65b1a3ca8899a1c580e4f0776136a5aa0d6240177d2650", size = 2755976, upload-time = "2026-10-06T10:52:21.011Z" }, + { url = "https://files.pythonhosted.org/packages/1a/30/1bffef5f0c961adcabb1846ffc83677edfbe0f04aa5b1825c8ed3b5f8506/asyncmy-0.2.16-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:75f4ad92c6e81e7e9660dc93d1720a5a318059304eb9ded112ca49dffa4f7ee9", size = 2242626, upload-time = "2026-10-06T10:52:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8c/d43362017e8e946f8ef28da3434a0105a4a33127cf367755553919273da5/asyncmy-0.2.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf36db8a319f1e1ca4facc0b55aa0521528ba850359e5b8120b2dd483e15cde1", size = 2224787, upload-time = "2026-10-06T10:52:23.291Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/a21ae6aaebeb5045c758818c4c6a605c426814fd70b8b6afa697e059add2/asyncmy-0.2.16-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3266def84b8b2ae6e71ff4ccaf1577e00030d0eec66a0c2aff0aa5589fdfa1cc", size = 6723050, upload-time = "2026-10-06T10:52:24.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fd/3beee4e556e1f62014c64ef3784ad80eefdfa752d25dae842f28d099a799/asyncmy-0.2.16-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31674278284ab9054fc8b69ac24d99748338269949cf79dd7c8cec9bd0cd0c2e", size = 6736165, upload-time = "2026-10-06T10:52:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/05/89/43fc5ac81887527ed50c532d3c6858dd9b4a97481cf00fa746da1eb515e4/asyncmy-0.2.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f4001c803c370ebd989d39febb8834fef4f66202549bd1e08513bd36d14df8c", size = 6467590, upload-time = "2026-10-06T10:52:27.172Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/bd12f7ecc3be153d06ed8e42414ea3cda8a193ca703499b04fe15d17e8cd/asyncmy-0.2.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23884d17d593a1e1adc0d797a0c2778bb40c081b3ed951186f0798206cfa8e0a", size = 6520994, upload-time = "2026-10-06T10:52:28.689Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/5dd22fe0484c7ccd8636bdbf8c4a7a381de51d6ec44aa118e381f674d7b1/asyncmy-0.2.16-cp314-cp314-win32.whl", hash = "sha256:fa5711c9f31c4f7061bdd508265a08b9770e87a64fbb0d3adc5314c4adef84b7", size = 1984266, upload-time = "2026-10-06T10:52:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/65/cc/b8d9a3ce3efcc860bddb8ada67af4b5f5a748fb64820c8a0ad17c95b5963/asyncmy-0.2.16-cp314-cp314-win_amd64.whl", hash = "sha256:d6bbb409f2829d9bca9a53599a9d8ef8429f7368d5b8ba30ecb8b13762e760d8", size = 2088335, upload-time = "2026-10-06T10:52:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/01/43/e5f40d2959f508b5b0eae0f78a1e06f711480cf787b1cd127984c4c92fd7/asyncmy-0.2.16-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5c56c535960002fe28464db2803dc765f009793f5c159d2bdb27789d95822197", size = 3073133, upload-time = "2026-10-06T10:52:32.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/b1c16ce3bcc620d5ba6dcd8353b0ca1a42e9debd71de7d0d56b4ec525f49/asyncmy-0.2.16-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05b49abf8de143b7f809dc26116caf1d16a818510f6324ebc2d1b36edd3f7bf4", size = 3049620, upload-time = "2026-10-06T10:52:33.684Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/0083427f2ef6aa5c5d5be9dfcba2b33507b5707a481f8a545584a50f374b/asyncmy-0.2.16-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ae8bdb8a4dfae7c210a863aa1cff3ca467da7269d98d120501d0528081f531", size = 12297510, upload-time = "2026-10-06T10:52:35.368Z" }, + { url = "https://files.pythonhosted.org/packages/11/12/00bd8ae2e1b1a5a2993b9498b24d38a9889a52e5db33eb6e88347e5a9ff3/asyncmy-0.2.16-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e175a4286774a14fd9c5e9301882033583e234cf75b874e80c8025a439e2c4c7", size = 11958268, upload-time = "2026-10-06T10:52:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/dd/97/00c2270bdbb6a721c0038bc586f0c3733e3f223d1864b5342b9b9d95b48b/asyncmy-0.2.16-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:09c2e97cdddd68355aa9f26a22dacc06f48d56ec75778c614f130f32e6016193", size = 11660058, upload-time = "2026-10-06T10:52:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/55d74e719860d00846baaedf52cbfd619527eeaa402f249545a5cf14b021/asyncmy-0.2.16-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1246506141dd5d2782096118f2c76ccb2d332cbfd56f611e6c652def4feca721", size = 11567753, upload-time = "2026-10-06T10:52:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/11afcc252c161d7f3e6125c4dbaac42805fa90751d2af3f9ab7bf798db86/asyncmy-0.2.16-cp314-cp314t-win32.whl", hash = "sha256:ddc8b367e2d50bfaaeb1d00da260182f332fbb7ce420057cee69abd83f01f5ad", size = 2568979, upload-time = "2026-10-06T10:52:44.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/438b1a6c0bdb125b96dd8f388e053e2d66b7c723d7111721560e37d47976/asyncmy-0.2.16-cp314-cp314t-win_amd64.whl", hash = "sha256:e9a89971bd7f5aa743d8a7121b2cb4a4b82b85361c14e5770375693600add878", size = 2778482, upload-time = "2026-10-06T10:52:45.654Z" }, ] [[package]] @@ -673,6 +676,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/44/a46089266de32a0d809023b6bf31b80a1f26a46954446a7ce695a565cbea/authlib-1.9.1-py2.py3-none-any.whl", hash = "sha256:8b9be8b1e5174dcbf5520e984f1a22dc2edae5b6e40a61d4cedbf86b97e9e69b", size = 266042, upload-time = "2026-10-14T14:17:50.695Z" }, ] +[[package]] +name = "auto-pytabs" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruff" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/ff/f5752f43f659ee62dd563af5bb0fe0a63111c3ff4708e9596279385f52bb/auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326", size = 15362, upload-time = "2024-08-18T13:02:28.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/df/e76dc1261882283f7ae93ebbf75438e85d8bb713a51dbbd5d17fef29e607/auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8", size = 13748, upload-time = "2024-08-18T13:02:26.907Z" }, +] + +[package.optional-dependencies] +sphinx = [ + { name = "sphinx" }, +] + [[package]] name = "azure-core" version = "1.41.0" @@ -702,6 +722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/d5/ae45ce8009d0d015304b206c7c4a6cbab02c03f8f07eedefb32b099ccee3/azure_identity-1.26.0-py3-none-any.whl", hash = "sha256:3c60d9682b3ac01507388d474cb5a5ffbe54152a96b6cdcaa7013da9315cf186", size = 195958, upload-time = "2026-10-01T14:17:49.705Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -711,32 +740,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "boltons" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/56/14c4a4931910a81ddeccfbe227925ea738e3c445d3e2af960f0bcbba1616/boltons-26.2.0.tar.gz", hash = "sha256:d39cfd15c1a1c3bd4d705c82252fa9edb8e4f5e8cc039f8e39afac7b1b47e92c", size = 242047, upload-time = "2026-09-07T10:48:17.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/ac/1f8e4f4855e382a67392ca3bba329cb93aba1e3039289c52a86bf337f7a2/boltons-26.2.0-py3-none-any.whl", hash = "sha256:41942cbce440211bbcf2487ccdc356964ad674f95a01068b8a0dd7a1d1fed036", size = 199536, upload-time = "2026-09-07T10:48:16.319Z" }, +] + [[package]] name = "botocore" -version = "1.43.117" +version = "1.43.106" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/a0/d0a5c44ad6a01f195681ddcb4ce203f06c712811daf1d15f8f8785e8a9f8/botocore-1.43.117.tar.gz", hash = "sha256:71993702832cc37c2495048b9945f2929d306fccb3fb767e9964e437e637fa6c", size = 16366888, upload-time = "2026-10-21T19:29:59.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/b9/10ca68d0092895d5ea60f485a61a9840d5aff9d732c66ed60da53a20b1d4/botocore-1.43.106.tar.gz", hash = "sha256:006870b3b4e40547232ad12c3bb4faec91bbbe0659aafaa3b7fa48a112c4ee97", size = 16271399, upload-time = "2026-09-30T19:36:35.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/94/050e35c9febf6dd2fce4a76be587ae4a323964865700467d9d7b082f7133/botocore-1.43.117-py3-none-any.whl", hash = "sha256:5920ef9b3002e1361846549078549d9154a6e0004c22853c01ce7b279d05d495", size = 16061765, upload-time = "2026-10-21T19:29:56.001Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/c51d3b54dfaa9267a687359d946dc2a1ca6a27fb4d381773e59b7bd72bf4/botocore-1.43.106-py3-none-any.whl", hash = "sha256:c1fb8818f9957cb5037f73db34ac1a12ba43562e31d6e7f9cb7e5fc64e1dba78", size = 15965506, upload-time = "2026-09-30T19:36:32.625Z" }, ] [[package]] name = "bracex" -version = "3.2" +version = "3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/37/adf8dd2cde958c2d086bdd87fc36b532fd824bf38f10be6348d65da0c8f9/bracex-3.2.tar.gz", hash = "sha256:0a3d40bfae4caae4d3bfd0ea715874799fab711c38fd4012cc35343e815405cd", size = 29771, upload-time = "2026-10-22T00:28:49.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/60/e2a6409041cb3f8ce7ac3d6917b5b30c39f81a0a5b2d414ec30f36fd96bf/bracex-3.1.tar.gz", hash = "sha256:35c69fc105f08d77c02722e8ae707c656ff298dd4d26c89bcb3c98fca8bc3614", size = 29040, upload-time = "2026-10-11T15:26:07.143Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/65/8030b03de8137d1aab7dd6ecfecdba2fc22fc849fa35959b024f62293a99/bracex-3.2-py3-none-any.whl", hash = "sha256:8f3e908a452f66707e1d6d91b3d7d7d4c649d248df18537c0dc710aeb9c30d87", size = 12019, upload-time = "2026-10-22T00:28:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/c1ba46e1acd235affd36ce1d3346320ddcda663c9482a9e5f34727db1d11/bracex-3.1-py3-none-any.whl", hash = "sha256:52002b8455658c21f3b6e5eb0f662b57ec9085f42709e58bc86eeab5bc4c37d7", size = 11925, upload-time = "2026-10-11T15:26:05.683Z" }, ] [[package]] name = "bump-my-version" -version = "1.6.5" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -749,9 +787,9 @@ dependencies = [ { name = "tomlkit" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/65/338597a14c53ae2b9d004031dbcb61a0641484b20469b5b7bd5250a37a34/bump_my_version-1.6.5.tar.gz", hash = "sha256:0fd40347bca359bdee9999c93e101d2ad4612cf10319bc5e81181e57fc313e0d", size = 1158255, upload-time = "2026-10-24T14:02:28.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/7d/6749796426303b7a96868fa7ee63db6cd4e101ce0044430d60f5596fb833/bump_my_version-1.6.2.tar.gz", hash = "sha256:29d111e4767b28e8e49bfbd29f8e332c0b17e11f58089f630b877d623d4d541a", size = 1125306, upload-time = "2026-10-18T13:53:15.227Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/bd/a8698a2143cccd7d5a5f23e7ec8520c2133e4083ccda1799531490fad17d/bump_my_version-1.6.5-py3-none-any.whl", hash = "sha256:7fc77cfa499edfa18072aaf7cc9e6697b8a66c7ba76f467c3c4fba4c554f2521", size = 67396, upload-time = "2026-10-24T14:02:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/91/27/4c27b17e1ebb22ade659c5561fbdc573c098957f53cf34fb57e753b4e0a2/bump_my_version-1.6.2-py3-none-any.whl", hash = "sha256:76cf619b8e91b650b3bcd9e6058fb32da13a56ef9e9c8d786a0b2ed6e98b9ae4", size = 65755, upload-time = "2026-10-18T13:53:13.97Z" }, ] [[package]] @@ -769,11 +807,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.10.23" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] @@ -1030,6 +1068,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] +[[package]] +name = "click-extra" +version = "9.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, + { name = "click" }, + { name = "cloup" }, + { name = "deepmerge" }, + { name = "extra-platforms" }, + { name = "tabulate", extra = ["widechars"] }, + { name = "wcmatch" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/20/e2d60c90277a668b10a00f7a74cd52a87ab0c85afd5b8975cc582601eafc/click_extra-9.4.1.tar.gz", hash = "sha256:065df41b1c8f6601bd9e550d79ac34a73ba40bfc84cfdb320cdcfe066c40a6d1", size = 1759076, upload-time = "2026-10-04T20:44:16.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/56/32f36d2a5218c2d89c40802649b943f847162de8849850f6a26a88a23a83/click_extra-9.4.1-py3-none-any.whl", hash = "sha256:046c650c1bcbfd14d6836df64156891d18e73c23287ef7b4c19d839308cecda9", size = 665674, upload-time = "2026-10-04T20:44:14.372Z" }, +] + +[package.optional-dependencies] +sphinx = [ + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx" }, +] + [[package]] name = "cloud-sql-python-connector" version = "1.23.0" @@ -1047,6 +1111,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4f/1ab0775120965381a7feaff9655b3163b4aa5380655a4c66e6a80ea90b9a/cloud_sql_python_connector-1.23.0-py3-none-any.whl", hash = "sha256:cf3d0423ed2033ef1afebfa29acef284da460b261adc86dcd480a34941451102", size = 63370, upload-time = "2026-10-15T18:22:09.107Z" }, ] +[[package]] +name = "cloup" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e2/d41446c6195eff0db3b671ddb202e39f42f9ea7c0dd15cd43fbf5cf0d7f7/cloup-4.0.0.tar.gz", hash = "sha256:83b0870ee863bcc85129e40e1b208bcfdebe4cd2142e9ce1d0daf7d276cab038", size = 252135, upload-time = "2026-09-12T03:23:39.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/34/20224fd9d52e8ce7e3b319b5d6b7126da25e89d00cd1a5692b4b46ed2d26/cloup-4.0.0-py3-none-any.whl", hash = "sha256:0678744a3a2753e1d78dd3ea5c6e4cbbc3b41561fd7a7a7d075dbc8dad74f74f", size = 55776, upload-time = "2026-09-12T03:23:38.564Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1070,128 +1147,121 @@ wheels = [ [[package]] name = "coverage" -version = "7.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/79/e78bd8f75a9b8e32233ce92d4607f0a21ce88a9156cace2884e7175a82f5/coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712", size = 991021, upload-time = "2026-10-22T16:06:50.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/25/79a66272edd250031aeee6db87d500a0bf76390d0d5f8c2f431fe354715f/coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3", size = 225837, upload-time = "2026-10-22T16:03:55.622Z" }, - { url = "https://files.pythonhosted.org/packages/52/a6/f5f6fefb2fc1305363927edad1d0a60ce6ef80d6c1a74a79e825f0f670eb/coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe", size = 226257, upload-time = "2026-10-22T16:03:57.164Z" }, - { url = "https://files.pythonhosted.org/packages/28/61/72889bac8eb44c51f8043edb227df551dfdca1fc4819a6c6d67b3f69427b/coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea", size = 258282, upload-time = "2026-10-22T16:03:58.521Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e4/03e4a5644df9b73de0d44d1b82acd18b06c42b5abc3f1433be8363fa983d/coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5", size = 260506, upload-time = "2026-10-22T16:04:00.116Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4e/949022286df7e59e8633a3a7e6635e0e6b53af7d57074cb0bf8f500373fb/coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e", size = 262600, upload-time = "2026-10-22T16:04:01.49Z" }, - { url = "https://files.pythonhosted.org/packages/27/cf/7d67f60df081006315699b7edb9402c1b47142caf1383f4e56ae8ff1a501/coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7", size = 264609, upload-time = "2026-10-22T16:04:03.286Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/9293d94afbc099990a085e88229a51c79b00828aba0f2adda32dc4cc16d1/coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c", size = 258382, upload-time = "2026-10-22T16:04:04.804Z" }, - { url = "https://files.pythonhosted.org/packages/24/c8/bcd28efedf262c074dae76f61098739eb02b15f3f716f80cc86f9885b8a9/coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177", size = 260299, upload-time = "2026-10-22T16:04:06.291Z" }, - { url = "https://files.pythonhosted.org/packages/b8/dc/503f55e5e2200933403c90d619d9f779b5060ddd338b1bdc63cd235592cb/coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca", size = 258314, upload-time = "2026-10-22T16:04:07.729Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b1/f1a57c36077cc516240636f7eff0d1427939ed795f096dd2c7769213a56c/coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba", size = 262239, upload-time = "2026-10-22T16:04:09.116Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4f/c2e59d4a132936c7c207b9b69ab99c0f27b14c3c93266e9922aa3b562b37/coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3", size = 257841, upload-time = "2026-10-22T16:04:10.509Z" }, - { url = "https://files.pythonhosted.org/packages/05/04/50742c0ee8b6c85826ff11737c93ed957fea4c9acf4b4b99804bdac1dd84/coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2", size = 259039, upload-time = "2026-10-22T16:04:11.916Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/83ad4bbbfcb6d045a33c14cdca313f6c9275737c67f3b7300222e3239a37/coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e", size = 227687, upload-time = "2026-10-22T16:04:13.537Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c8/dcc5a7effa79707eb8a61e699b141cff7c9b46036771274cfb48ea7b765f/coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2", size = 228290, upload-time = "2026-10-22T16:04:14.899Z" }, - { url = "https://files.pythonhosted.org/packages/d1/58/a8553e0dcd38d8136df63964638d2b588e0a4a916bff1bca996244fda693/coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241", size = 228199, upload-time = "2026-10-22T16:04:16.351Z" }, - { url = "https://files.pythonhosted.org/packages/0a/25/989fd3b495c4274ac0cf54cb6664326415e60183a7f8a73dd9bbb0b11e34/coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51", size = 225938, upload-time = "2026-10-22T16:04:18.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/cb/14726426bf3241657facbf0f7df7babd8f7d3fcf8e4609b7929f9dceecbc/coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40", size = 226166, upload-time = "2026-10-22T16:04:19.524Z" }, - { url = "https://files.pythonhosted.org/packages/8e/97/f95ed32cd3cfbe261a3598e694cb770cefc7b2d13ff013418d6f70a44dde/coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1", size = 258462, upload-time = "2026-10-22T16:04:21.079Z" }, - { url = "https://files.pythonhosted.org/packages/91/ac/eb4c31f741509a9ef48cf6f28385e98e9de0785599282252eee252ff8af7/coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983", size = 261789, upload-time = "2026-10-22T16:04:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3e/d9cd006d1a3cc5deab07beb29b86cb4a10dd84d66b330820363fb8d310d6/coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034", size = 262322, upload-time = "2026-10-22T16:04:24.94Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3a/4c872c6ae0762b09cdb8e856891bb3f584848cb60b6dda2bacb85fe2f3f0/coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b", size = 264600, upload-time = "2026-10-22T16:04:26.444Z" }, - { url = "https://files.pythonhosted.org/packages/df/98/1d41a2373fa98b423bcd70adb91d75e38eafb417955426dbb920b10016f3/coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e", size = 258678, upload-time = "2026-10-22T16:04:27.832Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/fe04b5e63e76506daa63d90bdfb00a53767fe5df18546b437d6226918d6b/coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5", size = 260210, upload-time = "2026-10-22T16:04:29.275Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/5c5a78dc19dd4799be9c6ec022316fb25ababaeacfaf0f52f24c56a57b43/coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5", size = 258375, upload-time = "2026-10-22T16:04:30.647Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e7/4c1d8eaff4d756fefd15d58c4953a78ab656782fd0e32c5a3465dba61aa0/coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56", size = 262422, upload-time = "2026-10-22T16:04:32.053Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2a/0b17c3e2e7b6ff1a5d92eb58915c0d1026668928f961d14dac73cf1b6096/coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f", size = 257875, upload-time = "2026-10-22T16:04:33.824Z" }, - { url = "https://files.pythonhosted.org/packages/d3/2d/eb0cbde0535342df0d17a667163030a3afb67e5eb9e3ad94af9540041584/coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc", size = 260361, upload-time = "2026-10-22T16:04:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/1be2a6db7eb655d0a1d899217c3f2469f9121cbbeddac35c1c8173b1482e/coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e", size = 227747, upload-time = "2026-10-22T16:04:37.102Z" }, - { url = "https://files.pythonhosted.org/packages/11/c5/201c894c57de81875d8b8cc527ba222aa350e8cfb183eb3f6df45d06ad8e/coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69", size = 228325, upload-time = "2026-10-22T16:04:38.989Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/196095339b19758cf83f507fb598bc42820ae124745feb3084662729e17a/coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438", size = 228131, upload-time = "2026-10-22T16:04:40.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/e6/1f01afbc9f4fa8ca5e1fa6fdf537a5c58b5765d1b4167ee284cddb9acc72/coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2", size = 227080, upload-time = "2026-10-22T16:04:42.325Z" }, - { url = "https://files.pythonhosted.org/packages/92/7a/bdb7e6260c4a211389aeac9b89dc31662ebe122153ad0ecfd156897912df/coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5", size = 227152, upload-time = "2026-10-22T16:04:43.737Z" }, - { url = "https://files.pythonhosted.org/packages/37/4e/276e7d5b3cfb124871bdb544c27b004a8b4e270e6bf8e7414149c54b5e5b/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398", size = 225397, upload-time = "2026-10-22T16:04:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7d/dac073db12f3c131598a6693253aded581e4d7d622a89864f11bfb0ca2f5/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb", size = 225705, upload-time = "2026-10-22T16:04:46.603Z" }, - { url = "https://files.pythonhosted.org/packages/00/45/877b0a9248e921080ae91d115c13192d17e308f5aee86bcc7f7bc2a54ed1/coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429", size = 225828, upload-time = "2026-10-22T16:04:48.065Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f5/2c48d47abec0e4590b302920334be5ee3854f96bbd22524feabb8d92b220/coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094", size = 226088, upload-time = "2026-10-22T16:04:49.513Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a0/ec0442f3f372c7fd742f49b3022edd392f79e37a0048e1fa333f56d01ec3/coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5", size = 257257, upload-time = "2026-10-22T16:04:50.982Z" }, - { url = "https://files.pythonhosted.org/packages/92/e2/eb0927ba93809f76eee69fa1e47999825fc7dbbabc3e0e1a2744f2677acb/coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669", size = 260165, upload-time = "2026-10-22T16:04:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a2/8d4c6055efb32099b15c9cc33d007b8aabebc4832f372a3274de5ba0afc5/coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd", size = 261098, upload-time = "2026-10-22T16:04:53.763Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/93412288688a7b03f152a734c8ef086e395108a0a6a746985d3967e220f2/coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722", size = 263164, upload-time = "2026-10-22T16:04:55.386Z" }, - { url = "https://files.pythonhosted.org/packages/3c/20/7eb8d805c4f1a212245cef20181dfbf39535357985073d49730ee9cf08d4/coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7", size = 257287, upload-time = "2026-10-22T16:04:56.976Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c1/1df29630b4ed17e135c76390afcc6bd273c3dc5ba2a21a76813a600fe98c/coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398", size = 259256, upload-time = "2026-10-22T16:04:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/a1/79/2365d1008d28aafe468a450a24c6c63363de794b5140223d67685cd0e205/coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e", size = 257178, upload-time = "2026-10-22T16:05:00.103Z" }, - { url = "https://files.pythonhosted.org/packages/c0/9f/f002952d295d772688dffce0d991e3822795b1e3aad158073712a5a5e86e/coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28", size = 261275, upload-time = "2026-10-22T16:05:01.462Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/f9e15fbb426be97cd679ee5d359151e046f103bcd76b5092c318c26ed596/coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95", size = 257029, upload-time = "2026-10-22T16:05:02.8Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4a/3c398f38694387334ff4c9f71afc8e15b4fa7db7bc0bea4d5b6032eb0df0/coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9", size = 259109, upload-time = "2026-10-22T16:05:04.241Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a9/908a3cf1a2335aec1c788682a06c2f742a62a4f987033f080de1c5fd1997/coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d", size = 227691, upload-time = "2026-10-22T16:05:05.636Z" }, - { url = "https://files.pythonhosted.org/packages/13/f7/8cdd7bfd8a63b3e6b133fc395ecfc7b1cb6f561926e7e2ad050de85b83e7/coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d", size = 228418, upload-time = "2026-10-22T16:05:07.057Z" }, - { url = "https://files.pythonhosted.org/packages/09/cf/0b3fc57f735cd3831ca13d0be5312a5477d5934066683c5d34018cab7541/coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83", size = 228142, upload-time = "2026-10-22T16:05:08.435Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/722462fe79912ebebc101497bd16c42413cde2dbae771017f1ec0acb84c2/coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a", size = 227197, upload-time = "2026-10-22T16:05:09.846Z" }, - { url = "https://files.pythonhosted.org/packages/b7/74/826b9c09f2ebfea803bccb391d935688a25309bff8e819fc446bc629c108/coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062", size = 227314, upload-time = "2026-10-22T16:05:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/6d/19/c8514d7f5e0659a96ff2bc7377119dca6d420e2761c5fc69ae37618734f8/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6", size = 225477, upload-time = "2026-10-22T16:05:12.761Z" }, - { url = "https://files.pythonhosted.org/packages/81/74/ef9433ce599f07d8633b4ce6e6d6921505c4ef3f7f9006f8eb66e2c278cf/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95", size = 225787, upload-time = "2026-10-22T16:05:14.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/26e58981940e8b3944c6a9941bec5c7f50840e7009a046712e3f3bcd9e6a/coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85", size = 226019, upload-time = "2026-10-22T16:05:15.798Z" }, - { url = "https://files.pythonhosted.org/packages/e9/63/fe175310815cc8357b83b773c9b4c20943c269f217dd825a37c4b4bfe324/coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e", size = 226169, upload-time = "2026-10-22T16:05:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b0/ac84761f5b4bd226696b0ec4606903d2ef7208abda0aec06c14a4218def4/coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7", size = 258092, upload-time = "2026-10-22T16:05:18.691Z" }, - { url = "https://files.pythonhosted.org/packages/e9/58/d8bdd65a661913bc12eaaa3e59742b2cb8b94f2e7f78e62184f0a58ca493/coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f", size = 261096, upload-time = "2026-10-22T16:05:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/5d82e2f0a75ea149e0a6412e137e1d610538772fdfd7199737db7fe234da/coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33", size = 262080, upload-time = "2026-10-22T16:05:21.568Z" }, - { url = "https://files.pythonhosted.org/packages/0f/cc/e3ebfd295d1dd47c7bc84a17d73f150465a37ab388dd68fe82b495dc79a0/coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e", size = 264558, upload-time = "2026-10-22T16:05:22.906Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9d/e2adff76d6544bbc6df7366d74967923c2de37ba889a006bfb875bf5857b/coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274", size = 257855, upload-time = "2026-10-22T16:05:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/d9/fd/ecd1e9cddf1c36b5d03c438568b1f53e63db84bb3656539d71b454484c7d/coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2", size = 259933, upload-time = "2026-10-22T16:05:25.79Z" }, - { url = "https://files.pythonhosted.org/packages/40/b6/e17a19dccdbcad494c64691f71c1dba3e55c30625bc29ffbb88ac5a0aa8b/coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76", size = 257972, upload-time = "2026-10-22T16:05:27.185Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0d/37dac534709a3fa43a937c8b0604839bb44b8670b83d143a4b78fec20d1e/coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048", size = 262271, upload-time = "2026-10-22T16:05:28.619Z" }, - { url = "https://files.pythonhosted.org/packages/cb/17/7c05186925b4d64a46c271dfd944e3f0c1ad89db9095c31a6df157a75df2/coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c", size = 257604, upload-time = "2026-10-22T16:05:30.07Z" }, - { url = "https://files.pythonhosted.org/packages/95/65/c241cdee66a0ff86c68f6d37328e735d10e3d30cd6ff66f356039eb688d2/coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e", size = 259714, upload-time = "2026-10-22T16:05:31.402Z" }, - { url = "https://files.pythonhosted.org/packages/db/09/e93233fe2405574ce0a31ed201c0cba3a860e085db5d2828bf12b7dc0790/coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657", size = 227914, upload-time = "2026-10-22T16:05:32.78Z" }, - { url = "https://files.pythonhosted.org/packages/d7/77/04979eb217edb94b94afabb4bcc179f60c746f68d3fd0b12688ba9195730/coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709", size = 228543, upload-time = "2026-10-22T16:05:34.118Z" }, - { url = "https://files.pythonhosted.org/packages/a0/38/155f2f2c6769f8c43eca8a5e581443faa0908828650090b97103b4a41376/coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23", size = 228396, upload-time = "2026-10-22T16:05:35.951Z" }, - { url = "https://files.pythonhosted.org/packages/52/55/d6751572d9445485c4c5a51ec6ae11caa5c11b7ef2448cdccc884f7f54d9/coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c", size = 226486, upload-time = "2026-10-22T16:05:37.363Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a8/82d37760e537bfdb9fdee285fa1e1b45bc153293d23e2331c7befedd1990/coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4", size = 226861, upload-time = "2026-10-22T16:05:38.777Z" }, - { url = "https://files.pythonhosted.org/packages/24/96/70ec00a9db6d7bdd3f914eba12057360769f5cb78a98456e71fe1937deb5/coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8", size = 266849, upload-time = "2026-10-22T16:05:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/77/d8/d99be3bfbb208512f039c80b31e404138a86678fc929a17acd5b7135a6ae/coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e", size = 269402, upload-time = "2026-10-22T16:05:41.585Z" }, - { url = "https://files.pythonhosted.org/packages/82/50/6cbe8b5a5060edbd21ce8109c4e900f8b7656e8b72ede6dc4d82dc0b8bfb/coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764", size = 272025, upload-time = "2026-10-22T16:05:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/bd/05/d3633add01e5518dfdacfdbd1be41cb5c581412dd8bccd663edf6ef7fec8/coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88", size = 273515, upload-time = "2026-10-22T16:05:44.324Z" }, - { url = "https://files.pythonhosted.org/packages/57/d9/b83387780271bf9ea9f96a3aeb01c1c302c4494a927923c4fa70cd170a3f/coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf", size = 266725, upload-time = "2026-10-22T16:05:45.713Z" }, - { url = "https://files.pythonhosted.org/packages/e4/be/8e80b29764ae3f4d70d44952eee3f3927eb36897490fe799e400adb7f5da/coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c", size = 269312, upload-time = "2026-10-22T16:05:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c9/e702f588fe8d83ca844f133270ad0a49bf650c8cf260551ff6f11a6b6bbf/coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b", size = 266740, upload-time = "2026-10-22T16:05:48.343Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/ee13ad14b714d75ef07c490b6e43969574a8b8b8c6d67bc741fcbacbf9c3/coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2", size = 271036, upload-time = "2026-10-22T16:05:49.727Z" }, - { url = "https://files.pythonhosted.org/packages/e2/bc/854e8912c11c54aaf416e74602000589c6efbbe1f251dc90202fdabddcb6/coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c", size = 266158, upload-time = "2026-10-22T16:05:51.141Z" }, - { url = "https://files.pythonhosted.org/packages/a8/dd/96432830ddf63bc42654de54eee77a0cd6949cc65769bda06f353323f23a/coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f", size = 268088, upload-time = "2026-10-22T16:05:52.92Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ee/230c6ec065a6629d41cc89323ceb54d37e2650c104f70fc108f4ee7c5051/coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa", size = 228229, upload-time = "2026-10-22T16:05:54.255Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1d/23eb6aadcb3c00764ff2961c0ff55f5dfa2543856d932b949bbc04fa68e9/coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673", size = 229154, upload-time = "2026-10-22T16:05:55.682Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f1/7df117e8e016b7a3ee459e833b3456f3610505afefd071f8727702d98439/coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74", size = 228726, upload-time = "2026-10-22T16:05:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a1/f9c24911755f91e6f451ca2e2c1804fbcbe9fecbc3310e341bbbbfa1d85b/coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559", size = 227196, upload-time = "2026-10-22T16:05:58.543Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a5/2b665b40c626e6bb40e5e8e23d91170b48b5f2f57186f46b07f2fac42386/coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b", size = 227315, upload-time = "2026-10-22T16:06:00.409Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8c/22b518d61b930b8a4d62905d046c76a03675ef19b466462011dfb16439a3/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c", size = 225490, upload-time = "2026-10-22T16:06:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/08/5b/d3b141455944e030cd4987f055f8c84a44a1a0b28d81ae93389f44051691/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f", size = 225797, upload-time = "2026-10-22T16:06:03.423Z" }, - { url = "https://files.pythonhosted.org/packages/03/24/aab2156983f7022361352640c583d08f0238e4cd7463eb921172001b4cfe/coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109", size = 226025, upload-time = "2026-10-22T16:06:04.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/db/94697b9ccf4ec94cdad07bf94e2a21b9702247cb212d2d5f18d2b9c3d0de/coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4", size = 226193, upload-time = "2026-10-22T16:06:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/a0/fd/db1c4ade4219831d590fff5aa5b7d9ceaf84586e8e60da6fff71b6182251/coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968", size = 258046, upload-time = "2026-10-22T16:06:07.51Z" }, - { url = "https://files.pythonhosted.org/packages/95/ee/84ed138990ed752abd73db4adffc1842d6152017d8e21809cc109a413fda/coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c", size = 261735, upload-time = "2026-10-22T16:06:08.861Z" }, - { url = "https://files.pythonhosted.org/packages/64/ad/c1e4367bbd1e7d562d6bc979c82b542f2040544e95a8cd995b694caf0e1a/coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771", size = 262819, upload-time = "2026-10-22T16:06:10.49Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3a/e8e8509b0940403eab15d86f70022790a01fce3e971a161919c544953f7a/coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f", size = 265194, upload-time = "2026-10-22T16:06:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/1f/77/d64bbdbb734b1059ebcde2b5df567f142a9948079e51af9b0abf8c9ec17b/coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28", size = 258410, upload-time = "2026-10-22T16:06:13.234Z" }, - { url = "https://files.pythonhosted.org/packages/e2/29/7cf81f08a254532c074eb52cfe6b12bb753d08f2997269c9fac8de359541/coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d", size = 260697, upload-time = "2026-10-22T16:06:14.595Z" }, - { url = "https://files.pythonhosted.org/packages/7f/56/bacea6dee2d5ccb31ab0cfffcee87ca7efa80ff048840209c8013aeaf02e/coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b", size = 258649, upload-time = "2026-10-22T16:06:16.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/97/fd8a6a63740f9ee177c53b59e2f4d0752369bb0fd154086a1e69aa1fb255/coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592", size = 262989, upload-time = "2026-10-22T16:06:17.85Z" }, - { url = "https://files.pythonhosted.org/packages/c1/37/7cd3d3520d45db3788456452b65446182c08f2a7c276f40c9dd11966be02/coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549", size = 257955, upload-time = "2026-10-22T16:06:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b9/80343a55e07d854927a915cc21a52e350ae1db9ead33f0248507b960785f/coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089", size = 260512, upload-time = "2026-10-22T16:06:21.055Z" }, - { url = "https://files.pythonhosted.org/packages/f3/32/e4c816f0cb3efdc6392bb276ff88359729f9dec605bdd3c1c7a44776f759/coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67", size = 227913, upload-time = "2026-10-22T16:06:22.407Z" }, - { url = "https://files.pythonhosted.org/packages/43/14/283120142ac281834808584249b89957de997c28573ddd65ae106b622121/coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e", size = 228532, upload-time = "2026-10-22T16:06:23.756Z" }, - { url = "https://files.pythonhosted.org/packages/b9/bc/83dbc6c3bd58042afaefcb0cfd7d7a2e3076989a2f3d9bc6e6edac819784/coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036", size = 228393, upload-time = "2026-10-22T16:06:25.163Z" }, - { url = "https://files.pythonhosted.org/packages/00/ed/9463d1cb1766a2570343a372c2871c7fb4d561e5051f029399f4d57ce947/coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a", size = 226481, upload-time = "2026-10-22T16:06:26.563Z" }, - { url = "https://files.pythonhosted.org/packages/ae/59/6536c7fa87be13236b4184fc395f14d18f622a5b8ef7448a6bfdf5f24ec0/coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6", size = 226876, upload-time = "2026-10-22T16:06:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/b0/bc/4ffae6038f78382c9ab6a4969cb5fc01d50b083d119851085807629abe88/coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569", size = 267241, upload-time = "2026-10-22T16:06:29.356Z" }, - { url = "https://files.pythonhosted.org/packages/c0/60/cae4b708b4217d33ebf66b9c57f8910a257346b3eaf054a78d73605ddc4c/coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca", size = 270079, upload-time = "2026-10-22T16:06:30.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/84/1225ab04e55a6a2e483e903a5eb0f8f081cb5325df833594534aa288d91c/coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36", size = 272775, upload-time = "2026-10-22T16:06:32.247Z" }, - { url = "https://files.pythonhosted.org/packages/94/ad/567f8dd9246a553aa8753732c41f0e0654e98e9a617d6282e64eae0e6aa8/coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697", size = 274110, upload-time = "2026-10-22T16:06:33.859Z" }, - { url = "https://files.pythonhosted.org/packages/66/5f/e0ee76895df40601e4aedffb6b8e71ea48e276a629fb4488b92cffffca14/coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2", size = 267775, upload-time = "2026-10-22T16:06:35.193Z" }, - { url = "https://files.pythonhosted.org/packages/19/6d/d19c6e45e940e5b9ba697b7be9a8e63cef73d90bdea45b670a59df466717/coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350", size = 270268, upload-time = "2026-10-22T16:06:36.597Z" }, - { url = "https://files.pythonhosted.org/packages/27/a3/b2731bb7aef2db800b330603675c3fa58a6497a05d5b5f8625f3b6285f68/coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a", size = 267298, upload-time = "2026-10-22T16:06:37.973Z" }, - { url = "https://files.pythonhosted.org/packages/50/75/d0c90a81b6b6607933109914dc8b0ef0a5f4cacf9482ca868ae773ee5d0b/coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3", size = 271899, upload-time = "2026-10-22T16:06:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/64/1e/f038f3a2614e9ef839a40a184713aca40b8ed79e626ab5cdc388771045df/coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03", size = 267380, upload-time = "2026-10-22T16:06:40.97Z" }, - { url = "https://files.pythonhosted.org/packages/c9/30/8d636e639b2d8b1934128573402a7caf66c8cfbc794fa02061d84f115c34/coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35", size = 268993, upload-time = "2026-10-22T16:06:42.361Z" }, - { url = "https://files.pythonhosted.org/packages/73/7a/219f6b391cb0636242d06c390a8add5af91afa3e92afd0380dbedd34f487/coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802", size = 228216, upload-time = "2026-10-22T16:06:43.714Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3f/514d294319c5eac11494394545de209079f33993e14526c220eab29b2a96/coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc", size = 229146, upload-time = "2026-10-22T16:06:45.262Z" }, - { url = "https://files.pythonhosted.org/packages/f6/82/c0d8bb0a67df838fc7bc6f4060ffc404fd1154fd18e624d83e6abc41aeef/coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c", size = 228716, upload-time = "2026-10-22T16:06:47.097Z" }, - { url = "https://files.pythonhosted.org/packages/c9/17/48032919343afa2906112d458aceda2043afdb24a2ee8eb4c60904d8ddf1/coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d", size = 217383, upload-time = "2026-10-22T16:06:48.471Z" }, +version = "7.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa", size = 971999, upload-time = "2026-09-27T12:29:01.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/fa/ce3baf63d85b730398d92a7162f486f3a5e4e2cc3382a02488b3943725ba/coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d", size = 223797, upload-time = "2026-09-27T12:25:54.756Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/9ba29c2aac7f756d479f03d45762120060f0f988788001001bf36e0e6fca/coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5", size = 224303, upload-time = "2026-09-27T12:25:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/0d6d60906dca7d28cc1e3fce12a9861801c4fbb6cbf220ad78cd059c9467/coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b", size = 254715, upload-time = "2026-09-27T12:25:57.755Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b8/9198b865679379fb165c689c64f6e11105ef380f6bd1c7673e83f73d9f5c/coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510", size = 256627, upload-time = "2026-09-27T12:25:59.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/79/9521462cb6072fe394701bc8974b74afd576c9c9355156c7844e1a86a42b/coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25", size = 258724, upload-time = "2026-09-27T12:26:00.691Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/8d7d5d633db9fe0f3182fedc731bf09f9bcf2366055735152504ad614677/coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a", size = 260692, upload-time = "2026-09-27T12:26:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a7/76cb09c89ba46d74d37428bf93251fc14fb0bbe9e05cc2a5ef61773d318a/coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5", size = 254800, upload-time = "2026-09-27T12:26:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/b6/2351c1979aaeb5b4a8091a75b90ca997ad60de36e181ddba267cf61dac97/coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6", size = 256423, upload-time = "2026-09-27T12:26:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f4/ad9a4f8b5cb2d494fa9452b546fe742ed2f9d3847cc14c05e36279a3e649/coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17", size = 254487, upload-time = "2026-09-27T12:26:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1f/a520470472f3e8b01169bf42162b1470c9ba992230432f62ca36269bf3a0/coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9", size = 258259, upload-time = "2026-09-27T12:26:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/09/d2/ff26d5938274745855fa61cfcba0245c88ccc10d98d2cbd96064f16cd5a7/coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035", size = 254216, upload-time = "2026-09-27T12:26:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/5d832d3b06785d9f53267e4f2724a9f60c312eee6ebed9063a461d0d3b45/coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878", size = 255125, upload-time = "2026-09-27T12:26:10.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/4d/1d33edbc2fcf7d99e384e393e712aa5a2ebbbd8409825357815982207976/coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516", size = 225898, upload-time = "2026-09-27T12:26:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7c/676df4882118756c4f8f560c954eddb93e166d84dda8c5f0b6a829689bde/coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d", size = 226369, upload-time = "2026-09-27T12:26:13.375Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0e/a457f4a461b3c5610d845137fdd45fa465e011a64c25af440518ab1f4e41/coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a", size = 226344, upload-time = "2026-09-27T12:26:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/f8296c63c5d542f3d21aed685e56b7031a419037d155bb3382fc0940d249/coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48", size = 223969, upload-time = "2026-09-27T12:26:16.753Z" }, + { url = "https://files.pythonhosted.org/packages/90/23/6f3dcb1423a0d43216e402ea1746e4a7c7c44f38896b97dd573790f56a40/coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99", size = 224328, upload-time = "2026-09-27T12:26:18.15Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8f3b6dc920e3fc6732f7678785a2091db439f186afbec30dbf2214d9b1f7/coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808", size = 255832, upload-time = "2026-09-27T12:26:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/6c45f15be4eca4ac1062c6a55a323286494c99726a7e58951fe85967ac08/coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d", size = 258571, upload-time = "2026-09-27T12:26:21.199Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/b54cbeba3ad89082c2e441278681859e538322cc34b84b2af7ebff00080f/coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c", size = 259680, upload-time = "2026-09-27T12:26:22.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/0dc65ec3d61930e1e4c2e371763b15eb4290896eb343a12d5d3091308116/coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800", size = 261945, upload-time = "2026-09-27T12:26:24.336Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/5fad7a61f2c14e08e98946fc31c1c7ffc1195061bf3fdc351db3be77a863/coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148", size = 256191, upload-time = "2026-09-27T12:26:25.89Z" }, + { url = "https://files.pythonhosted.org/packages/2d/47/74e5de9227b939ece9f64e729645ddc4296bea10dbfa98721c1333c8be2e/coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7", size = 257610, upload-time = "2026-09-27T12:26:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/2cf28d40b43645d1b72388fe3ee7f7c747533a6a9557bb8c24a7ae74fe1a/coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad", size = 255757, upload-time = "2026-09-27T12:26:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3d/7c149fd99fc8bbc39c80db5e688d1d39fd040be2ecb78b8335a51a55b9c0/coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6", size = 259822, upload-time = "2026-09-27T12:26:30.515Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/b283fce09d5995e227bd8e513358dd7471bedc0f78abc85a925ebdb0a2f6/coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd", size = 255325, upload-time = "2026-09-27T12:26:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/bf/91/f3325edf0c4223fb1fe1532b8dbef2a1d2f729459a9a7d1a44d073bae534/coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206", size = 257191, upload-time = "2026-09-27T12:26:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/21eb5e83ecf2eed523c4eb3d65ae513cd082c8fd1b6deb34c4cb6c332f97/coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a", size = 226034, upload-time = "2026-09-27T12:26:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/db/de/e3ad6d864c0833624b4f1f9b53f9e58e116c945e5e965c3f1e172c5e84cd/coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162", size = 226569, upload-time = "2026-09-27T12:26:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c1/bccc58ebe5489cc70628f635c1932fd371f5d7da850dbcf960f95f4c4afc/coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02", size = 226372, upload-time = "2026-09-27T12:26:38.406Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/8eb4f220ef24f84fb27d852d4f9bf83e0c73ec1a4a08dd9a87e3f4529739/coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9", size = 223993, upload-time = "2026-09-27T12:26:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/40/23/d4bbaf0c154e0b0c2b5264890dbf6ef098dcb50ec8f2469be9490d191660/coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b", size = 224368, upload-time = "2026-09-27T12:26:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/7f/48/fc1e88fd571ec5cb38150b7f89f7696ca1bdf9920e01432febb69774cc85/coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204", size = 255358, upload-time = "2026-09-27T12:26:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/1d/56/6785397d07c29c8e70fbb9a07e97d062b43c21ffc5f12385917847f09f63/coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5", size = 257955, upload-time = "2026-09-27T12:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/27/3b/c8cdd07721e5f99abd81cea970d971997f99bf158c0b85f51bd284179c8b/coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7", size = 259183, upload-time = "2026-09-27T12:26:47.208Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/606b192fe43d32574ec6238549d48de588fdcc18485682a5ec0a8ac357f2/coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541", size = 261325, upload-time = "2026-09-27T12:26:49.084Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/eea481f8b0305ceeb33f081a5f47e298391dbd1b589de0c4b3b3aa50d3f2/coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc", size = 255531, upload-time = "2026-09-27T12:26:50.509Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/dedbf9aea1457b120c27ac10b8fc2a357f37fa2b54c3e7286d42980a0a2a/coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967", size = 257329, upload-time = "2026-09-27T12:26:52.005Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cb/b25c19d5bb2bd0f2e4e27fe8e2ffcae80c7a91ae181c0dc749ed60e9b1a4/coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7", size = 255294, upload-time = "2026-09-27T12:26:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a2/892c5c5f4ad44b7b2ca009aee705191f3f268f15052244f2f9e3539b2e35/coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23", size = 259444, upload-time = "2026-09-27T12:26:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/ed/99/a562537deba0a3e370182ae71c149be796c39d8087365f17a09188f27145/coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8", size = 255111, upload-time = "2026-09-27T12:26:56.851Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/854ec68641a9b3362ff068a32dfa41637299761617ef253791dbade6fc76/coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4", size = 256882, upload-time = "2026-09-27T12:26:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/db/0d/748e4518b0ac0f9ff2687c248a6e5f8c0737306e709372632a2556f84443/coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865", size = 226045, upload-time = "2026-09-27T12:26:59.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/fa/6e46edba66a183fe4d99d4bb52c173287e9b8dddabe0888d24cb8210e580/coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6", size = 226581, upload-time = "2026-09-27T12:27:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/9ef6845367600b336ff75d000444a0d32497d6972c833141bd39356abf68/coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79", size = 226409, upload-time = "2026-09-27T12:27:03.116Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/577fc0803dab4155dcf808faffbdd7b159256781c0874a8586e17b81b149/coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a", size = 224138, upload-time = "2026-09-27T12:27:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/9e/e3785ba3ecba2bd11efc74bfe2801ca4b78c4480b15a375648d809a59da3/coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee", size = 224445, upload-time = "2026-09-27T12:27:06.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/963ff22d3fd27117da3b8cc442f5bdc91196f783321e1a8ff0ec43476772/coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b", size = 256112, upload-time = "2026-09-27T12:27:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d4/a306940c81c6ae759e82fff27d20b7fdc6896e422b821f51313cce212b6c/coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03", size = 258727, upload-time = "2026-09-27T12:27:09.927Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/d3d99d93b02517087aa05bc0cf2d04d372956b849e5443e059079901429b/coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda", size = 259909, upload-time = "2026-09-27T12:27:11.55Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/39dd599181726758dd185ae4dc0c0ab3aeabf7ca70e68e145060feeaaa16/coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c", size = 262481, upload-time = "2026-09-27T12:27:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/91ee43f6ded411460c359d7e1aebde4d6fd8f00a2e5394182d9d212eb23c/coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9", size = 256012, upload-time = "2026-09-27T12:27:14.91Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/e9499ddc33197bd7eabcb1118ca81756fc874457b324e2b479a4804b2ad2/coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4", size = 257898, upload-time = "2026-09-27T12:27:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6e/c081cb5991a0afba99f9c4ad6c74a5fce9513a38ddc64e3e6680c6fed9af/coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90", size = 255938, upload-time = "2026-09-27T12:27:18.19Z" }, + { url = "https://files.pythonhosted.org/packages/b2/42/1c3d819e8f9b6eb01c2fe90874d67a8882adb9507e0bbb09361ed131ea89/coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95", size = 260368, upload-time = "2026-09-27T12:27:19.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/4f/d70eac07901fd587b6ab05e659b52afe13959992aa5113bf6cce059cc572/coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8", size = 255620, upload-time = "2026-09-27T12:27:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/6d87af88317d3d9a9b18a9ca1bc1673eb516917f296e579d0d4a55cb3490/coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6", size = 257521, upload-time = "2026-09-27T12:27:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/79/bb/90c2641170d2fa1a6757b3f8450ba2740197317b0ddd749e9604b914e886/coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011", size = 226248, upload-time = "2026-09-27T12:27:25.153Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/d8d0478bb02c8eb0ae20a496fc80c40fcf4d3450bd184300d682ba2d28a6/coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b", size = 226732, upload-time = "2026-09-27T12:27:26.907Z" }, + { url = "https://files.pythonhosted.org/packages/32/3f/0001da22155b0a8ce063ec0f7e64ecbe17b373f306e7a74435f6d6accb72/coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb", size = 226645, upload-time = "2026-09-27T12:27:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/6d8813aff9b8b8586691a9d33c43c5604f7227622574da7cdc3d91a86861/coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf", size = 224871, upload-time = "2026-09-27T12:27:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/444f3a4981ac2cda40fdcf4cc9b56a4e1a33c222abeb33e51ed3e3eb2a6b/coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036", size = 225123, upload-time = "2026-09-27T12:27:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c1/980681cd7b33eb66ac835044116ef0a92e11fcc7bdd866cc89d10b1130b9/coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3", size = 265049, upload-time = "2026-09-27T12:27:34.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/87679875c33bb2191f0f05544a1cc9adcc940fe0c35443a10f2df753dde5/coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831", size = 267717, upload-time = "2026-09-27T12:27:36.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/64/5d372776d6eb523d4e93bafba2253f96984e3b18261c4cc56a50863c6d0d/coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf", size = 270047, upload-time = "2026-09-27T12:27:37.96Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/44082ff0cbf9f97d0043f57970a71204097ec7ba606361a9fd2065393669/coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681", size = 271360, upload-time = "2026-09-27T12:27:39.766Z" }, + { url = "https://files.pythonhosted.org/packages/b8/17/9a215efe25b5e0ecc87c89dbe525c4a87d14d87c8c0c7316ef140a5f6f3e/coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0", size = 264736, upload-time = "2026-09-27T12:27:42.072Z" }, + { url = "https://files.pythonhosted.org/packages/a2/da/7f0a31af8e448107d4d32844bd684757f51ea907bc0c68c8fd537b2123ff/coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb", size = 267600, upload-time = "2026-09-27T12:27:43.85Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/3bfecbd3366b775bacdcb3330394d356cf384b5d8f5b2146ac4b14b252b5/coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7", size = 264767, upload-time = "2026-09-27T12:27:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3f/5d62163732d87e4a0c4710a0eab30f0fd6a2d480112abe2029f014fe8c9d/coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64", size = 269026, upload-time = "2026-09-27T12:27:47.787Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/8e4579f225426535085a9be371cc75e3b026d058d679b80affbdfb4c3ef0/coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20", size = 264147, upload-time = "2026-09-27T12:27:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/ef1f77e2c3f7bb03c2b13b9a2006f88700fdd75535ef158d70049f425c1c/coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce", size = 266376, upload-time = "2026-09-27T12:27:51.551Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/0cb2bf4428830dec971c718c2c841a039c084415c99e67281f5a72841aab/coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46", size = 226585, upload-time = "2026-09-27T12:27:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/da17121c16667fd84998e972200ae226a41540f6ea4795776c6d99e8976f/coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c", size = 227378, upload-time = "2026-09-27T12:27:55.778Z" }, + { url = "https://files.pythonhosted.org/packages/74/89/01179c62d1b7e6e33bd5001566b02d7f778cf33d3ec1e81e94ca170c517f/coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91", size = 227064, upload-time = "2026-09-27T12:27:57.496Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8", size = 224140, upload-time = "2026-09-27T12:27:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24", size = 224449, upload-time = "2026-09-27T12:28:01.299Z" }, + { url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09", size = 256060, upload-time = "2026-09-27T12:28:03.185Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843", size = 259106, upload-time = "2026-09-27T12:28:05.15Z" }, + { url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1", size = 260674, upload-time = "2026-09-27T12:28:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982", size = 263139, upload-time = "2026-09-27T12:28:08.771Z" }, + { url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34", size = 256591, upload-time = "2026-09-27T12:28:10.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6", size = 258708, upload-time = "2026-09-27T12:28:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4", size = 256562, upload-time = "2026-09-27T12:28:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0", size = 261077, upload-time = "2026-09-27T12:28:17.215Z" }, + { url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a", size = 256034, upload-time = "2026-09-27T12:28:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63", size = 258034, upload-time = "2026-09-27T12:28:21.242Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e", size = 226252, upload-time = "2026-09-27T12:28:23.102Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae", size = 226728, upload-time = "2026-09-27T12:28:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495", size = 226648, upload-time = "2026-09-27T12:28:27.136Z" }, + { url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75", size = 224858, upload-time = "2026-09-27T12:28:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55", size = 225139, upload-time = "2026-09-27T12:28:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2", size = 265090, upload-time = "2026-09-27T12:28:33.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2", size = 268254, upload-time = "2026-09-27T12:28:35.135Z" }, + { url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1", size = 270695, upload-time = "2026-09-27T12:28:36.959Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f", size = 271853, upload-time = "2026-09-27T12:28:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d", size = 265644, upload-time = "2026-09-27T12:28:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0", size = 268530, upload-time = "2026-09-27T12:28:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283", size = 265154, upload-time = "2026-09-27T12:28:44.684Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec", size = 269799, upload-time = "2026-09-27T12:28:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5", size = 265196, upload-time = "2026-09-27T12:28:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c", size = 267266, upload-time = "2026-09-27T12:28:51.361Z" }, + { url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35", size = 226583, upload-time = "2026-09-27T12:28:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5", size = 227374, upload-time = "2026-09-27T12:28:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8", size = 227064, upload-time = "2026-09-27T12:28:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f", size = 215754, upload-time = "2026-09-27T12:28:59.075Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -1314,25 +1384,48 @@ wheels = [ ] [[package]] -name = "duckdb" -version = "2.0.0" +name = "docutils" +version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/7c/dfdf5e9cebefd032f57f09b224993e861ecb55f61e36c709544b04b44f70/duckdb-2.0.0.tar.gz", hash = "sha256:09de511ef0dc76a5213e8d3d87a8b89bb4d3e8a6fe73a9f60dc001269946b44f", size = 16771077, upload-time = "2026-10-21T12:51:13.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/57/ec7c1a01ab43fc13c55c17cc54a5740886e04ae830aa420ac00614b1420a/duckdb-2.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a3b38d8cdbef10007c64b65efc4087f799c4dc606785aa5f2a425ed13658ca01", size = 37006608, upload-time = "2026-10-21T12:50:38.611Z" }, - { url = "https://files.pythonhosted.org/packages/84/36/e1ea89acd74917ca654d5adc278765f0116e07540abf4134d5cf9f527363/duckdb-2.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:9bcc9921be0e1b2404b96a7fb447a8b80fe2f3a919cdc976ca6c39951cc8f2f5", size = 19691094, upload-time = "2026-10-21T12:50:41.839Z" }, - { url = "https://files.pythonhosted.org/packages/4b/81/2895b9ee3858dae7a57f791d7dbd4f116e9fa8fd3c71b9a377dbd4d292a7/duckdb-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61a244faae9fd787e30f4c76888769ec49e79736c434f698e8178a00af8348f0", size = 17603089, upload-time = "2026-10-21T12:50:44.167Z" }, - { url = "https://files.pythonhosted.org/packages/04/95/0c8b5b95ac9e14ee83090a78048fbb23fb9f8abacd63e11a5eee7fcb31e1/duckdb-2.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcdf10dfbc2c1a6bdc5b2a94bba5b15481e552c5745c2af1994617b0eb6bc750", size = 21637946, upload-time = "2026-10-21T12:50:46.444Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a1/9c3780faada6e6a3edc5f9364cf19810e6e1ea90a7d5fd8fffc48b42c269/duckdb-2.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a3601121c7cc50d0b70412995634682fafcb58a0156d5d663dfd0b3bbe30a33", size = 24351483, upload-time = "2026-10-21T12:50:48.902Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/0729ac5d9857e5f0bde7262961f80e578a29945a444169886094cc9c69f4/duckdb-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a387d4907b535a4bd82ba8aede6809d0bf023dfbb21362832f3c9848aea7743c", size = 15370691, upload-time = "2026-10-21T12:50:51.59Z" }, - { url = "https://files.pythonhosted.org/packages/09/13/392991ec9dcb22811c7029009184a910bd5cb69fbb7a626f36f151215535/duckdb-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:62bea9a1786be98d2906197c67a4089cc8b34d66d19e87e5543d8201e81323ad", size = 15731407, upload-time = "2026-10-21T12:50:53.889Z" }, - { url = "https://files.pythonhosted.org/packages/41/28/1fb52f9ea10205a55a07c32e37cd4fd68fa9d1dc2520a12405ec9bb59cac/duckdb-2.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cb1cc30318397c3bcffa6b683196293698052588de3c1ba1e144ba0c72560ce9", size = 37003294, upload-time = "2026-10-21T12:50:56.489Z" }, - { url = "https://files.pythonhosted.org/packages/07/5b/fbe92644709fb9162cc51753f92f892b1f333658079d1f11efaed43341d2/duckdb-2.0.0-cp312-abi3-macosx_10_15_x86_64.whl", hash = "sha256:46c7a299ff5f45be9068cfc0ceecb3fd574af9535de8cb6908ae8b52d2846cbd", size = 19689207, upload-time = "2026-10-21T12:50:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a3/4d23938223626980cdcf18fd6b0dbf040823b5cf15beabbf4ab7ee276631/duckdb-2.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:b5d758b53751c930e09d371230241005ea5203c8edc65a1e1fafca83fcc906ef", size = 17597612, upload-time = "2026-10-21T12:51:01.818Z" }, - { url = "https://files.pythonhosted.org/packages/92/4a/985fb9d93d7f9f8e82092ddfe05ec8c9a1d0df7bdb0f562a10ced30e3e8f/duckdb-2.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e38daf0acbad9de7ce892d402b68409519291ee16af3c2cba85ace3b085c7eaa", size = 21655920, upload-time = "2026-10-21T12:51:04.36Z" }, - { url = "https://files.pythonhosted.org/packages/cc/33/01ad2535ee897db2bad70cbc800655167c53afd0b955eed84f0759c2f417/duckdb-2.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab51ec57d329d6f3df37c0287b55d71c3fc07bb54b61d1afed09104f953b1431", size = 24371005, upload-time = "2026-10-21T12:51:06.86Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/9e8ca1242b98f04041bef09686ecfe098b41f820aa23240f63f360b64eaf/duckdb-2.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:f61c96547ed432f54dae42a717fd88563eda55cb47549715fac9f61f770c205a", size = 15368860, upload-time = "2026-10-21T12:51:09.395Z" }, - { url = "https://files.pythonhosted.org/packages/37/81/6ea5dc1050f86ec4dd038a2774766b42247ba6104e7785f13a0c600e2b33/duckdb-2.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:c5e6708efaa8647e0914a16f1914354419e529a1925929950e7d610418e45873", size = 15726489, upload-time = "2026-10-21T12:51:11.643Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "duckdb" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/0b/d65ea3be00ea79aa276a8388bec588a9cbf409ce637c6d306e5316210d15/duckdb-1.5.6.tar.gz", hash = "sha256:166a91dbfacfc0c9f08cc76c0243cb6d3d4296bfab5bad72a3cfb63140a5b7c8", size = 18032957, upload-time = "2026-09-28T13:38:37.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/e5/01e03d30b7ba33a030a4269fdca16ce445ce10f9d29b84a10fdbe0636ad2/duckdb-1.5.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c88700d0ee68ad149a0cc624df21b0f21efc136ea2449aaadd7cd0c9a564962a", size = 32757482, upload-time = "2026-09-28T13:37:29.916Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/7f7be626a4649a3948ca646c84d6afc1a00121f292f98e6f0d9ed68330df/duckdb-1.5.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e4f1b10a8b8ff476eb2b73955590fadbcef978da1167c593114c5edf763960", size = 17372997, upload-time = "2026-09-28T13:37:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/1a/66/9d57573729348d800a0eebdd508f1a833d3714f72e984fef79b47f0e6c45/duckdb-1.5.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34623eaabd2c66ba5c20f1a39486321c3b7d32e4e0e001ced95f81e3372dd361", size = 15514224, upload-time = "2026-09-28T13:37:34.467Z" }, + { url = "https://files.pythonhosted.org/packages/57/ec/97f595214b3a27b4ca42b8cab6d8121c06f3537dcc4d2da7bca0332de4c5/duckdb-1.5.6-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56c0f71c6bee982e9c30568bb12371bf66b26bf129c75d8d7f60bc69d6590a2c", size = 19428776, upload-time = "2026-09-28T13:37:36.689Z" }, + { url = "https://files.pythonhosted.org/packages/68/4a/ab59f4c1f76fb89e28d23f19b2729538e0723c8d328a07e1b8c37f9ee128/duckdb-1.5.6-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b108c04c932b36c2fa4e41110cc1c3c8cd510eb49f065f92d050be8e6929fd", size = 21537771, upload-time = "2026-09-28T13:37:39.548Z" }, + { url = "https://files.pythonhosted.org/packages/31/4f/9306c442ecad76f2a4d19f249e7fc8861f139dcf748315102eb69de8ca56/duckdb-1.5.6-cp311-cp311-win_amd64.whl", hash = "sha256:dda311932cf5aae955a53fe28a4fc1700c2ab5fa02dc1f165abdd5ec6c39141e", size = 13179009, upload-time = "2026-09-28T13:37:41.981Z" }, + { url = "https://files.pythonhosted.org/packages/a0/40/8a370e998293d3ebbbac4d926db30bb4ac5f700851a06ac31e7093bee386/duckdb-1.5.6-cp311-cp311-win_arm64.whl", hash = "sha256:df5ae02af278e084f54a9730a9f4f211ed736d0bd8f3bc12af925c2effb5b33d", size = 14046340, upload-time = "2026-09-28T13:37:44.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/d0ab77a0a1702a43171c93874f44c1f6481e30038bd3987df0d77a16a5c6/duckdb-1.5.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48d07d0651aaeac2c3974afd37599970154b7b79b54c18f27c319c14ccf98d9d", size = 32810486, upload-time = "2026-09-28T13:37:47.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/b22201de5377faa3be6c38d5f3eaa504cb480392a448bed6a4d2239469b4/duckdb-1.5.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79de3dfa8705b1ba0d59e7e3252e40ff399e0afd12f485502a6c7bf7c2fd809a", size = 17405278, upload-time = "2026-09-28T13:37:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/f9cfb1493bbdc2f095693a402e42dce1192077f9e11573f00baed6a748de/duckdb-1.5.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcccce20965e6986cd083fdf192c461685ad0b93cd1ccd0b2a8207f1185f078b", size = 15532943, upload-time = "2026-09-28T13:37:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/53/04/f65ccfaa5a833f2e570c4a140f03c8f95da416da9fe8ed08401f81f8242a/duckdb-1.5.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce89a1025a5317ebe9c520876c48032b5247ac574865486648b1a004f6009875", size = 19454940, upload-time = "2026-09-28T13:37:55.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/99/be75c788a492f8d77b7a1cdc1b19939ae7be0007f2028691ad371a1a33ee/duckdb-1.5.6-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc9619ed7d4ffa117b5155d84b44794366bb6635178d78ed5e13a6024845c757", size = 21568087, upload-time = "2026-09-28T13:37:58.191Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/889f8508960e47c0a7c75cc5bf57cde8512fc24f8db7b3129cca5388da42/duckdb-1.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:09ff51b230219f0d8b47fc8a1e17fb595ba9fab0c3d96a6de4d00b8ff86b3cf1", size = 13190189, upload-time = "2026-09-28T13:38:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c9/baab503364a68309f8368c88e77f5341e7d94927bdf3e6d703f0e5035f3e/duckdb-1.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:b8d795c8b2d5634b3269f974aa97f1fdf878f62f032317a52252a151b693fb1e", size = 14021977, upload-time = "2026-09-28T13:38:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/a476197fcba557738a588ec844747a19bc0a24b0e6f1809e308f29d68c0e/duckdb-1.5.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ae352646374cacf48e9981cf031191c494865192fc436d13667a2531fc5d1da3", size = 32810376, upload-time = "2026-09-28T13:38:05.148Z" }, + { url = "https://files.pythonhosted.org/packages/0c/6d/5466a2b53ddd557644dfa47a763f68748efccdf282e6ae7c4f1bcfb3da69/duckdb-1.5.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a1261e90785e9d29953293e44f60fa073bd1137098924e8de21a037a861b051", size = 17405385, upload-time = "2026-09-28T13:38:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a0/bf87071170835ee4a34fe764fc11c1c6e7040a0e021b36c1b6f834a4c22f/duckdb-1.5.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97dd7a555b8f5298b76bc7d48a11cb2c64336e8de9bfde783cffb86ea9f54807", size = 15533132, upload-time = "2026-09-28T13:38:09.681Z" }, + { url = "https://files.pythonhosted.org/packages/31/e0/38095c8e140ecfbe847519ac07bcba94301b8fbb76b2870015e33e07f179/duckdb-1.5.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364992ba1089a2b327391cfcb68fd0bd0ce9090cf293baef861a0ba6847abfee", size = 19454994, upload-time = "2026-09-28T13:38:11.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/21/61dd2876bbaa69cf77d7b5c620e52e8b25faae7096f4d2e4a812b52095d7/duckdb-1.5.6-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:644f54ce99b3b61844bc9a3fe80e0aecb1ea4084b1fffc4396d1569db6111679", size = 21568700, upload-time = "2026-09-28T13:38:14.258Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/100730e7785e85268be4d4d5bd62cfc8314e261d2f42efa208243eef35cb/duckdb-1.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:ced693d33ddcee2e5345f077d342c87d2aaa80e41c514e64c9ff2d4e5963c251", size = 13190707, upload-time = "2026-09-28T13:38:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2e/bc7f44eab4e89ee5c1cb427bb1168ad021d985042e6841ec0694c3d3d501/duckdb-1.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:41ecc75bb9328d72d154a705c1a653d2c5c60f686a5c0c6578aa80020753c884", size = 14020962, upload-time = "2026-09-28T13:38:19.007Z" }, + { url = "https://files.pythonhosted.org/packages/fb/62/a8a30a4c6b94c0861d348ed5633b963f6745a5525527530f02f3c1a7c931/duckdb-1.5.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:aa21d2ad803b2524326e8622d7d96b2bb1ff1d5b60368e1978ee805df9c21fb3", size = 32828003, upload-time = "2026-09-28T13:38:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/71/b7/1dcca0005eb8c67adf9fc06bf0cbb1d2bf4ea1974cc89e7a7c2ad66aac28/duckdb-1.5.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a1b2ad27d414068cbca06c55cfa802eece10f86ea4812ff082f8ab4cb25fc85", size = 17413912, upload-time = "2026-09-28T13:38:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/e3ac175443550f3464f2d95731a8b0aae9b4dc3875c3a186c352262b43c2/duckdb-1.5.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c79c6d222b1d015cde73b5139087186b00db65357fb4e2c94c2308fbbf465a72", size = 15543122, upload-time = "2026-09-28T13:38:26.317Z" }, + { url = "https://files.pythonhosted.org/packages/9d/08/cc510a7952aba69d5cdca17f3ef61c95713d86143f2ee9aa3e097d38f50b/duckdb-1.5.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1052b8050ef5696e2c0d8c836949c72f3dd11f0690466acbea739613e8e2750b", size = 19457946, upload-time = "2026-09-28T13:38:28.877Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a5/6f8099d9a5a02ddff89e5c85875df3465054845b0920fb0703fbdf8dd2ec/duckdb-1.5.6-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c5e485e59613b8878d1670bcaa7a010f53c5a4da5ae8e08863e5e529ca6182", size = 21575132, upload-time = "2026-09-28T13:38:31.231Z" }, + { url = "https://files.pythonhosted.org/packages/9f/58/762f7159662d7859e201fa05ca29f306795daeabf84f3e087215a966b001/duckdb-1.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:ebcbd09cd8578ab1093393e9b16289cda0e8f1791ac595bf00eb5bad75c3cf00", size = 13713963, upload-time = "2026-09-28T13:38:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/64d165db322de13f5c3e75d377b6b9694df1821155ad1fa4b14b04601abc/duckdb-1.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:820a8384faef11cd86068ea48c5da57ce2d8f1c7b3d2bdb9be3398317a7c3728", size = 14514368, upload-time = "2026-09-28T13:38:35.676Z" }, ] [[package]] @@ -1358,6 +1451,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "extra-platforms" +version = "13.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/7e/b92eb5fe6c14b00c1469357622ee32e8b96ba8247b90b9da4cba3743ab32/extra_platforms-13.12.0.tar.gz", hash = "sha256:883feefe546402e1ccfe6f53b3f248dbaee3cd72d82e137549769db452c4712c", size = 494651, upload-time = "2026-10-12T18:13:30.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/26/7f61812ba2cc540edd3889802e90deeee03cd9569fafe41c64f1e35e03f7/extra_platforms-13.12.0-py3-none-any.whl", hash = "sha256:68a0cc9a16be81e6a9795bf413da7e14b1e8707dee260a47bedff7b6a5db58b3", size = 107721, upload-time = "2026-10-12T18:13:28.764Z" }, +] + [[package]] name = "faker" version = "40.43.0" @@ -1372,19 +1474,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.162.0" +version = "0.141.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" }, + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] [[package]] @@ -1459,11 +1560,11 @@ wheels = [ [[package]] name = "filelock" -version = "4.2.4" +version = "4.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/08/d8f81287742010e38daf3632380c8d80ede7b4df2716f8ca3d0a2b087d44/filelock-4.2.2.tar.gz", hash = "sha256:b29ff2ae9140bb17b3bc77c648dc4f010d9ac0955eccabff4f2b01dde859f787", size = 651339, upload-time = "2026-10-18T07:26:44.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, + { url = "https://files.pythonhosted.org/packages/4e/53/35698fff350cb99c49ee01ae5cb1eb4408567792a286314ea984b53b5002/filelock-4.2.2-py3-none-any.whl", hash = "sha256:1851d2f14ca2901e98069d68333b33ad7409f56904e17b48cc0c32a0db8ed73a", size = 162415, upload-time = "2026-10-18T07:26:43.115Z" }, ] [[package]] @@ -1602,21 +1703,9 @@ s3 = [ { name = "s3fs" }, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - [[package]] name = "google-adk" -version = "2.13.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, @@ -1644,26 +1733,25 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/2b/9616b2a498b8f247c1a9bef8c630e16151dd3a8ed768fa3118a961b19689/google_adk-2.13.0.tar.gz", hash = "sha256:bc2389c9d41bfadd9f0c87d6bfd8d5c7eaf1c449e8906079600b8f3752ddaa88", size = 4444119, upload-time = "2026-10-23T18:26:34.717Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/c7/52dcd19f95d412a60b2e42bc4fc586f48106778d03de7c250840d6a40ba1/google_adk-2.12.0.tar.gz", hash = "sha256:93dfb7ff5e27d18f96520f59939faa8a7b6c21c7908161022702a3c208a37b25", size = 4305241, upload-time = "2026-10-13T16:42:58.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/1a/6da4a8048b7cd47681928daed5d4d1c3ef971b3847fcc3cbc63f94df39d2/google_adk-2.13.0-py3-none-any.whl", hash = "sha256:bec4c4ee0aa3e63a2243c23365c5aef666ea539bc48c78dfbbf004dbddfc5f00", size = 5157034, upload-time = "2026-10-23T18:26:31.805Z" }, + { url = "https://files.pythonhosted.org/packages/d3/65/3706e52bac2f8ac1328caa7da28dfff8a9086fce5004619960f91c7f7bb4/google_adk-2.12.0-py3-none-any.whl", hash = "sha256:79f89ea94ccd87e4ac8c893e81c44ed2ce5568d05406a95c1d10bcef0ce0ff5d", size = 4998446, upload-time = "2026-10-13T16:42:55.26Z" }, ] [[package]] name = "google-api-core" -version = "2.43.0" +version = "2.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/1c/83011225511b8a0aeee9633d58d8e6b5d858ea5502e76117bcefb620245d/google_api_core-2.43.0.tar.gz", hash = "sha256:d67355a22d995a009b6c5ca98744ea35fbf4f9c2c006aa0588c0941d1ceb5216", size = 282948, upload-time = "2026-10-22T23:43:15.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/5e/ea85dbdb29e05a1dab94be7e4c07e5099b7048074f9ededf7d39636a878a/google_api_core-2.43.0-py3-none-any.whl", hash = "sha256:dd588ae9de5e372be89b87c68b1e1555fe89add1809d623182b773d14f96859a", size = 223488, upload-time = "2026-10-22T23:42:44.924Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, ] [package.optional-dependencies] @@ -1819,7 +1907,7 @@ wheels = [ [[package]] name = "google-cloud-sql" -version = "0.1.2" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1828,9 +1916,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/02/bb267b1a9917eb95f069da6116c5928fe9fba82f315271228fc0d1bdbf7b/google_cloud_sql-0.1.2.tar.gz", hash = "sha256:c42bcd1b30f5d481463386da3713e35e3f326d80a41fbe89c5836a2a3dcd09ba", size = 1260461, upload-time = "2026-10-22T23:43:30.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/e7/1fadfb0ce27e040427d763865a3d75db79ff3e994983bbf680344c5bbe7a/google_cloud_sql-0.1.1.tar.gz", hash = "sha256:58b56e81698faa3ff9ca551bbb29bf0c947f79a488bf0b0a5e33c538ba244491", size = 1084532, upload-time = "2026-09-02T22:23:20.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/31/b35992f23a20b7858261ff2eeaf4279b9b9adf09d0312bda4b4c3ef56081/google_cloud_sql-0.1.2-py3-none-any.whl", hash = "sha256:220b4f6433dea8c8e421e9d51b683faa6ddc87b0b9078b2a1dde1f391b6f46dc", size = 1263305, upload-time = "2026-10-22T23:43:02.113Z" }, + { url = "https://files.pythonhosted.org/packages/b2/26/d239e6550ec996712a21d9f0db798624532172cef82f5c45a493edcc832f/google_cloud_sql-0.1.1-py3-none-any.whl", hash = "sha256:6df562d0cacf73fb503cd51ac1952497dea9c2f58187f60a6d295d5accf72ec9", size = 1096495, upload-time = "2026-09-02T22:23:18.974Z" }, ] [[package]] @@ -1885,7 +1973,7 @@ wheels = [ [[package]] name = "google-genai" -version = "2.33.0" +version = "2.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1899,9 +1987,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/a4/b83b378b4a2fbbfb300c3fde0ff9f443f049c33e2accedf06bc0ad9bf393/google_genai-2.33.0.tar.gz", hash = "sha256:fced7f746da45264b34bcf6044a6fcdb91ebad1825e95c324ee48addb6e557ae", size = 758627, upload-time = "2026-10-23T00:06:28.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/69/6a3d3839ab56ec17d7923c8679b38955e286e0a9793df10ce6d13bc5f122/google_genai-2.31.0.tar.gz", hash = "sha256:71518537de2ba967a61960cba844691dca3dd4ff07726c01a6917d132213541e", size = 745679, upload-time = "2026-10-14T23:52:24.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/3b/9ae8e16c5b2177bfdec1c64769a93eea9093de684e0bbb95c10c77645fef/google_genai-2.33.0-py3-none-any.whl", hash = "sha256:f3f8ab696b35caf95c222e05268740c1ccd6934257890506be73b126150d3b14", size = 1222487, upload-time = "2026-10-23T00:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/64/1e9ab799830101ccbdcb67376a3f12b59e51f57643862999fd31af6938a1/google_genai-2.31.0-py3-none-any.whl", hash = "sha256:c87b9d495a24b09dedb29b852a201bea69ecd0b3f6c2561dc68f4055451b1b90", size = 1208245, upload-time = "2026-10-14T23:52:22.766Z" }, ] [[package]] @@ -1918,14 +2006,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.76.0" +version = "1.75.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/b6/bc095b23a3e0d978e75c8c18cabe4c11f791837410670e4486f0d539bfba/googleapis_common_protos-1.76.0.tar.gz", hash = "sha256:71db0e44a1d4c2dd024ebad44ca67129cc1e2892e4861df466764749d5ef8642", size = 156250, upload-time = "2026-10-22T23:43:36.263Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/2b/6ce81972d5c8cab9705fddce3153be63222d9e12fd96f8baba5038a744dd/googleapis_common_protos-1.75.5.tar.gz", hash = "sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72", size = 156513, upload-time = "2026-09-29T19:26:14.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/6d/78dc67a9e779e594b49bf62604034d3a6aa428a55afc729cafda4b7333e1/googleapis_common_protos-1.76.0-py3-none-any.whl", hash = "sha256:59e13344a9f3651aa201a03e2bee815a3bf43a4baf1f9ff8a9b62a840ce25fac", size = 307707, upload-time = "2026-10-22T23:43:10.445Z" }, + { url = "https://files.pythonhosted.org/packages/65/b9/6b29500a1c581ff4d77fd83c6568d068bee06f1b139fb6eb0a4f2d4bce8a/googleapis_common_protos-1.75.5-py3-none-any.whl", hash = "sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d", size = 307737, upload-time = "2026-09-29T19:25:48.735Z" }, ] [package.optional-dependencies] @@ -2019,27 +2107,18 @@ wheels = [ { 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]] -name = "griffelib" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/e2/0f5d8ed08766bc6dcd3eebf5a6623d83cbca33a6e4c90aef57156ce745ad/griffelib-2.4.1.tar.gz", hash = "sha256:40c028dd2e623f464423f0e11e3c553158f1cf4f1368239a370534d73e1407ff", size = 245153, upload-time = "2026-10-22T11:20:03.705Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/02/1156d5f5307f573b901b14373848b84e69ca6cab09169b8cc97d16a181f9/griffelib-2.4.1-py3-none-any.whl", hash = "sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce", size = 172983, upload-time = "2026-10-22T11:19:59.843Z" }, -] - [[package]] name = "grpc-google-iam-v1" -version = "0.14.6" +version = "0.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/24/0ef24ccdd91ce07be7d6466d39becdd4fbb33e52426ba2391fe334d24bca/grpc_google_iam_v1-0.14.6.tar.gz", hash = "sha256:fc0a683ace0a149184fcd08d667dd64d51ae65e5698e86f7136aa6002a8b5373", size = 24088, upload-time = "2026-10-22T23:43:37.377Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/e0/0524fbc82835c22e711ac3e7d7dfb3ee997f99f5eaa22ba6b31cc17b753d/grpc_google_iam_v1-0.14.6-py3-none-any.whl", hash = "sha256:042705a3ce306f289b9c6eafc3453b23f953bab955adcac3c36545758ccd681a", size = 33089, upload-time = "2026-10-22T23:43:12.096Z" }, + { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, ] [[package]] @@ -2056,77 +2135,77 @@ wheels = [ [[package]] name = "grpcio" -version = "1.84.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/3a/5c8e0eea2af3d30cb20d33ac325a98d9b799c069d63101ca4da20bcc776a/grpcio-1.84.1.tar.gz", hash = "sha256:446e7876b9b991fa124c1d414899fa7cf95f89a3e71237ea0161863dc0b2c09f", size = 13421202, upload-time = "2026-10-22T14:59:43.986Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/99/8c0a3739dbc75bf509cf2a79a35ebb27e73cd7a1f9be25af842ae66e4749/grpcio-1.84.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:a5ea4e8d1e9745ba6c2c9d14ed0861e9496a8051bf9fd8a17b667d9b9a0c1019", size = 6420007, upload-time = "2026-10-22T14:57:39.037Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/c8261b1d56715d09c922145d12427abc1b1be5afad031336d7b00bad7d8c/grpcio-1.84.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:11c1d07c58922f5dad022b422d3f7998755c3e35e2f1949bac47cfd7a5eb31fd", size = 12325248, upload-time = "2026-10-22T14:57:41.384Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4a/30f23e49f1b8ab11ccd24cfa146524df5478f45db7167006580cdfeaa321/grpcio-1.84.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a22342e59b5b2f4c5ed276ef72a3668ec84f38d2f5fad8a64af0b3618e510eb", size = 6995366, upload-time = "2026-10-22T14:57:44.399Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d8/fd238758e4725f11d509c440de1b03a77c53d5f213fd45ccfb5f4f775aff/grpcio-1.84.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bdd1181edaf42b6e79a457789fd43e85858db1df3861e4e5128ff4d6a49cb638", size = 7747707, upload-time = "2026-10-22T14:57:47.241Z" }, - { url = "https://files.pythonhosted.org/packages/47/54/d6544884ef836bea968d3ed7abf179c4d33a794dd8a8b1c5cf002fff9620/grpcio-1.84.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eb881326607275ad2d2d17dce09b823472414dcc559e8f694d5607fe8af3cbc1", size = 7153395, upload-time = "2026-10-22T14:57:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/06/9c/094cd0afd21348e65d5750a7fa4cfa5f8ba6ca354fe6aff9f1163f4c5d0e/grpcio-1.84.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:772ff116d0d31b2817e23fb9a1dfa52fa9fed08be0cd87b593450f034e2968b1", size = 7702992, upload-time = "2026-10-22T14:57:53.602Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/254432337e14beb08bc7f29fc10a86ef149f672124073f2e0d446dbf8538/grpcio-1.84.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0225c1e29d1506594e75ac46fd7b327bbfd24c6306a3c479b2164c1c4e87901f", size = 8758120, upload-time = "2026-10-22T14:57:55.728Z" }, - { url = "https://files.pythonhosted.org/packages/04/29/528ccb437076233d524e09e59320625b8ca9d94f0fb0198703e7100f3bff/grpcio-1.84.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce9e360d0a4a329b925ae8b0bc49d97d663bcce50822eb862873798a86eb10", size = 8110925, upload-time = "2026-10-22T14:57:58.639Z" }, - { url = "https://files.pythonhosted.org/packages/24/5a/e4005d02dd65884a525f49ba0aa0371967b7fb40f00d35818ab397d91a93/grpcio-1.84.1-cp311-cp311-win32.whl", hash = "sha256:b87f8696bb78a31328e815629d320304c3d5cca1c654d3fd7e5518685bcfb8d2", size = 4460566, upload-time = "2026-10-22T14:58:01.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fc/fc040230b60529a5df349945eb1e4f8a201607a6e3d70f3465d80e801393/grpcio-1.84.1-cp311-cp311-win_amd64.whl", hash = "sha256:e44786d5c166944a53fb1fa3384d03f1dbade6f68007be6d0c5a1a0971cbaca6", size = 5225017, upload-time = "2026-10-22T14:58:03.113Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/34a3e64818b1fdaf73b6c873f7d085969f20c2aff114c8909cc7ee0cb2b4/grpcio-1.84.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:db2dbe5380dccc2f5df967f31308a744e65d46e21b473a9c3ca033c4299ec3f4", size = 6381194, upload-time = "2026-10-22T14:58:05.289Z" }, - { url = "https://files.pythonhosted.org/packages/77/bf/5f1922cafaaaa7b2b6a0f6b0614783efa8502f16c8dd9c7b8b131a3dc9d2/grpcio-1.84.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f7533921b52ef94210437dedd3911e00b7642ca8d11eda64eb1715778630715e", size = 12304970, upload-time = "2026-10-22T14:58:07.714Z" }, - { url = "https://files.pythonhosted.org/packages/af/f9/77e9991fd5ae7d754b78b3c8e8eb028ddc886a480b86b934ece85f727f95/grpcio-1.84.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e0dcc7b588e7b7dd3363317296983db23a26af8baa5884db15a10fc0c8534186", size = 6949905, upload-time = "2026-10-22T14:58:10.92Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a2/af693a3bd3fb43cd97e4ad054ad5b117814c1a24cad048130e982a189f0f/grpcio-1.84.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a92bb13dd1b31a16f631ab415710de59f404c37364c018e7486496fb449a89da", size = 7714870, upload-time = "2026-10-22T14:58:13.622Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/456ef643a796ba04ad737e39a820c27291c238db87f7d168355dbdaf7b4b/grpcio-1.84.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:474408c92b27c823ea6aab2315e2ea52d5a4e0040b37ce1e65e89d9149c48205", size = 7121552, upload-time = "2026-10-22T14:58:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/a9/bc/c98f29a74b772ba9025f13243dc9547807583b1d111282bdcc53bb4f8db6/grpcio-1.84.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:912694cac76adb4f45563b38ae4d06d77a2081e8ed5cb20a3918c1affba83c63", size = 7673450, upload-time = "2026-10-22T14:58:19.009Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2f/713bbfaf1cd7ce3a4626b8c0e93ec3f7493d8f52ccdb1ddb02fc98fc7d7e/grpcio-1.84.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f821954e4d89e97f7d51c03073d6964e177f2d11181550a5a22c082fac614f43", size = 8725415, upload-time = "2026-10-22T14:58:22.23Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ef/21db8160edde5646a0f688c9f7e97bf6106965e4e2ae1dcfe9649fe2db01/grpcio-1.84.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02612f3c19ed3dd885e2fff1236bf5629e5b43b5aa84c3290b0d5f62f26b0ef7", size = 8090355, upload-time = "2026-10-22T14:58:25.039Z" }, - { url = "https://files.pythonhosted.org/packages/34/0e/a762461094659b13a86a027461a6acdf4734dbbe17b19bb193191801b570/grpcio-1.84.1-cp312-cp312-win32.whl", hash = "sha256:380b58bf32707b4e3019c9567dab30375bd21f9a5c98aa24401aff1e73f3b262", size = 4444024, upload-time = "2026-10-22T14:58:27.015Z" }, - { url = "https://files.pythonhosted.org/packages/51/b5/2dbcb95b7b3622253bbb09b79dd87787dd05709303e571cc0eb150f860d1/grpcio-1.84.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a221fe42e444cd0099752028810a289efafce1b5690dfe19a831d2a2fe6ea62", size = 5218662, upload-time = "2026-10-22T14:58:29.124Z" }, - { url = "https://files.pythonhosted.org/packages/0d/57/f25d35af33566daec7c9eec09c65e06f510cea2cc392bc2988af26250a4e/grpcio-1.84.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:9c24b1ebbdf867b33d74868044fed5c7c5a6a6e00787140b5f582572f7a7e633", size = 6393057, upload-time = "2026-10-22T14:58:31.938Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d5/68c3c5804bc7b05a0a544ac68439ff7eb493d5d394e5361bc94e60b6be0b/grpcio-1.84.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:af0f0c10281c2b62c68868e0a98de24f89c31308c68dbf19320d30c3a329aba5", size = 12302330, upload-time = "2026-10-22T14:58:34.375Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8f/d25a5d4d4f34e99ec31869ad3b988893f15d5965625cfc35a5faffb9889f/grpcio-1.84.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c1c04d2876ccb063507a976602bc88b5c31330982e650c620bf8e730b120c9d5", size = 6954893, upload-time = "2026-10-22T14:58:37.408Z" }, - { url = "https://files.pythonhosted.org/packages/88/f8/fae50f0e28281be328614ad772118290d0f804d9be5be2d5708e7ea8d9aa/grpcio-1.84.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cf14e9cf02d12628f20ec205d699838c47e2d3741690fbf14ae626bfa32ac0b", size = 7723209, upload-time = "2026-10-22T14:58:39.537Z" }, - { url = "https://files.pythonhosted.org/packages/59/4d/99dfd9cfab7f748d52cfb9177e8967d5cec0a50f148d4e3b9a0bf0b1220c/grpcio-1.84.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ce575e400e0dec9484f5d038fa12d6b3919d493c2beb897a23b9a32fa3bfd12", size = 7125010, upload-time = "2026-10-22T14:58:42.037Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cc/7354ebd3d4c1bef08b062b9f07d154141c2b9043f677a219bc7943070867/grpcio-1.84.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6e9be93efe763bb2b12a4c0d5fc13082f24de6cbb53a9d11078c9d0c8cdb2b8", size = 7675985, upload-time = "2026-10-22T14:58:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/37ccd26e1f7a7576f53627b45e222ac89ea3465b3f02836f480ac26e4dce/grpcio-1.84.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f216832aa84ad7e697ae1ed993c33905d27e27cd1b02016af1441be2091e287d", size = 8726955, upload-time = "2026-10-22T14:58:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/88/cf/4887c033734182cecd12d3a1f6f9e7f84e9a5589a1ca2f564ff2ce8f2a29/grpcio-1.84.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a4a06d757e7ea5ddf636a4c40914aa36fb4f7e2a03abf727a16e61822331343", size = 8086854, upload-time = "2026-10-22T14:58:49.831Z" }, - { url = "https://files.pythonhosted.org/packages/d0/2c/34aca5188055d8e5342a5a672f6d321f331c8e1e944ea7d3650ee71f4943/grpcio-1.84.1-cp313-cp313-win32.whl", hash = "sha256:4052f86fdb5105ac8b02c77d17cd9beff78790a0c3117afdd5316154cf4348a5", size = 4443070, upload-time = "2026-10-22T14:58:51.851Z" }, - { url = "https://files.pythonhosted.org/packages/68/25/5c315847ab3bd6d3b9f3caad178596eb1affd5aafd02374813b27bddf708/grpcio-1.84.1-cp313-cp313-win_amd64.whl", hash = "sha256:b7452ccc592e40e4167adec46195d85387c8036f0aadf6f96ab6ed931205d0ce", size = 5220505, upload-time = "2026-10-22T14:58:54.464Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d8/bbc616bd123ddc2fd1f53f49e40cf537f7dd9c483f47bb21be61c4aa0ae1/grpcio-1.84.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:42ffbbc11fe2e4b02a4b51b090f8ca6727414748cf551539665b5592372263e8", size = 6393649, upload-time = "2026-10-22T14:58:56.534Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/71e0903ca23c483aad588daa7744f9b01c149db9cbfa13eb24ef5f813a2e/grpcio-1.84.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f2482ebe5e3a9c6d1f99859e135cc0ca370362e3b2a94cb6304f64943997bfcb", size = 12308221, upload-time = "2026-10-22T14:58:58.92Z" }, - { url = "https://files.pythonhosted.org/packages/28/01/a0207b967ab991bc009030763e3c7e6280cbeda1de06351e16034d34547e/grpcio-1.84.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b8fb04fe0ded6fb1c5edd4c90ed39193d7b6226214685111b25a153ccb33a52a", size = 6963837, upload-time = "2026-10-22T14:59:01.562Z" }, - { url = "https://files.pythonhosted.org/packages/38/74/a4171ad8c4b0e80371afb27503cad580d4cae2f116b56adbb9dafa7e3c76/grpcio-1.84.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ffc0be893d8df55f0d23afbd3b776ede444847260a4a00ac80326a32dac91ad3", size = 7722977, upload-time = "2026-10-22T14:59:03.784Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b8/73e85d67437fa4324933cc625a6a8e8e5f22fbdfe6757d949a6958b87479/grpcio-1.84.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccd8be89a73d276336cbf223b22bd5c677d9fd15233100f208845c6a0c7cbc72", size = 7126919, upload-time = "2026-10-22T14:59:05.982Z" }, - { url = "https://files.pythonhosted.org/packages/03/60/734b781ca01f562e6e01c84ef8e762f1ebcfccd02b29be0b77513fc26da0/grpcio-1.84.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ced0c5ef2709964aca16e6af6f74f682b57872d2f7c7c553916c6b86dac64fa4", size = 7685628, upload-time = "2026-10-22T14:59:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/03051aa012430b2dbd15fbf9af42482d945197b7886c77c557475d1bb4bb/grpcio-1.84.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:28caf3df58096e2cb06f0f7e672160e65e9df5e053f392594d93d33a15fe1ef1", size = 8728232, upload-time = "2026-10-22T14:59:10.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/83/c87911b98ed766d7084d64053dc06f7855267f686b673dc0bb1ac8c07aee/grpcio-1.84.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f4b64099afaccdeeb987240af47aa1fc14f57285d3d2ff000c7327228624999a", size = 8088730, upload-time = "2026-10-22T14:59:13.431Z" }, - { url = "https://files.pythonhosted.org/packages/88/bf/6b18690790c0c4d7062749785bc8e515881a8a4058f46cb2925c86adc9e0/grpcio-1.84.1-cp314-cp314-win32.whl", hash = "sha256:116b4796da0e7fca3fdf6edf422ea7521c25d8c4be64ab7e3b5e2a77c832b0ed", size = 4527749, upload-time = "2026-10-22T14:59:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d9/ee50fe67481254c45c9b709b31ba468e4c90925018f4507c424369a9abd5/grpcio-1.84.1-cp314-cp314-win_amd64.whl", hash = "sha256:3875f4c4caf0c54ac370b66322f0bf2d46e9d21414be36ea8b7f618badb3b87c", size = 5359432, upload-time = "2026-10-22T14:59:18.331Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c0/fc3341990f75cba2096d07ea5e7a8f78af8cb36f6b1fd78660b8c1220e11/grpcio-1.84.1-cp315-cp315-linux_armv7l.whl", hash = "sha256:775f98cb476625936061b91e3c1da229112b212a6b2a70fefc9c87e5aaf9fba6", size = 6393689, upload-time = "2026-10-22T14:59:20.402Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f4/6b9a70760bbd8e927f31452ed2ba33cc3dc1a327f1b46c44f65ddb6dbf0a/grpcio-1.84.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cfdc7530e5707ab6e5c80fc624f89e26ddf8f38d3e89e5226dd7e55c080e9b65", size = 12325454, upload-time = "2026-10-22T14:59:22.805Z" }, - { url = "https://files.pythonhosted.org/packages/ea/87/e44f1c73d4c003a10a8c9b348b44296e99ec5623b70fb441ad250f4b532b/grpcio-1.84.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c61a0d5c60506b2371444716b56d6c633d3c56a17c703566bcca39604c02c12c", size = 6984778, upload-time = "2026-10-22T14:59:25.385Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/efd7828266b9e5ff0764af8420d52add5acfc13382d073596f15d7dfc183/grpcio-1.84.1-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:79b3ee4683290b3cfabadc718a008c3474073b585bc450231a842c21c2ef1f27", size = 7073415, upload-time = "2026-10-22T14:59:27.503Z" }, - { url = "https://files.pythonhosted.org/packages/80/5e/de257fc887b5a4e955173fb28a76a39fdb69f12e0d99987ed723c937c419/grpcio-1.84.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8ae49802f09cc272546592b3656207de6f719e50a6a0d1654e613aaba6336ac", size = 7165478, upload-time = "2026-10-22T14:59:30.083Z" }, - { url = "https://files.pythonhosted.org/packages/75/b0/66840129d46b06a5159277206a1f706ff755d725fdd0652e7acf6961a930/grpcio-1.84.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1e25f45414b3a53bfc1fe43b980f9d0cae9e194a62541d5f6bef0846758162ac", size = 7707719, upload-time = "2026-10-22T14:59:32.284Z" }, - { url = "https://files.pythonhosted.org/packages/10/0d/92682af96b05c1d5e5ce1ea82a3969b221c9d58112a097ff637946e395c6/grpcio-1.84.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:de3c31abc0d052511744a817506e03534f14e32dadab8ec19f63f2d4facd05f3", size = 8079074, upload-time = "2026-10-22T14:59:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/0c/69/1e426aa52c23640430eed1728c0f7c1167bb64eb7b0a31bb5ce9a500f73c/grpcio-1.84.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6ce0835704f30d4954aa0a68352fc9cc30242cc85f39d7b6cd20c0790e05925b", size = 8118408, upload-time = "2026-10-22T14:59:37.082Z" }, - { url = "https://files.pythonhosted.org/packages/72/66/037c29a3668a32f2dbd287ceac43286797ffe14c95e7234db75c42ea10dc/grpcio-1.84.1-cp315-cp315-win32.whl", hash = "sha256:43d8b84b3e2b7505e4902ab6288ceee699cfec5435ed879bb53f876b2966ee33", size = 4527284, upload-time = "2026-10-22T14:59:39.362Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/4dea740311428c40fa0da674e0ecfa28f572809cba4cbc9c568918c82d2e/grpcio-1.84.1-cp315-cp315-win_amd64.whl", hash = "sha256:c8e1c15e81f65efb317906754fdba4a85d2eac9cff250d8a3fb3ccb3a4e52db9", size = 5360072, upload-time = "2026-10-22T14:59:41.328Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { 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]] name = "grpcio-status" -version = "1.84.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/18/53459706d79cb7c144808d941cb48f099764776d483a85922129662070fc/grpcio_status-1.84.1.tar.gz", hash = "sha256:690ecf476c82d6d98703a80578cc0ec6d0b5222187f783bd0158abd28396cb42", size = 13993, upload-time = "2026-10-22T15:04:10.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e2/b9a35118c6c231a3710a0b4f2174da87f2c09045e743c1edc221b4659900/grpcio_status-1.84.1-py3-none-any.whl", hash = "sha256:5a1455bf5b39fe2a3d09d71661e59c97622c73888ad11f13109ce82220b88218", size = 14639, upload-time = "2026-10-22T15:03:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -2193,15 +2272,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.16.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" }, ] [[package]] @@ -2320,7 +2399,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.16.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -2330,9 +2409,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" }, ] [[package]] @@ -2385,6 +2464,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] +[[package]] +name = "imagesize" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, +] + [[package]] name = "importlib-resources" version = "7.1.0" @@ -2435,14 +2523,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.7.7" +version = "1.7.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/15/48ff246732232d35ee234228def22b52455fcc55577f06146602399a29b0/joserfc-1.7.7.tar.gz", hash = "sha256:471d0fd12a5a0d1c0942de280f39efe6f597878315033270b504d4d006d25510", size = 237446, upload-time = "2026-10-19T00:30:29.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/94/80fea1514b7c6d7d37804d3fe9ca81455f633347fc98731bd71ffe1faa17/joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea", size = 234990, upload-time = "2026-08-29T13:05:42.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/9f/934db284aa22f37af59708e382903860d4b81329c137b1b0ed2745daeb65/joserfc-1.7.7-py3-none-any.whl", hash = "sha256:06979cfbab78bc802f14c0358f32969826cc26720e13cb38926ffdc86bc0cb2c", size = 71758, upload-time = "2026-10-19T00:30:28.498Z" }, + { url = "https://files.pythonhosted.org/packages/67/c5/82addfd375e5ee6520644e0553e4aadde92d668c4fc99cc716d337fe7bb3/joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159", size = 71269, upload-time = "2026-08-29T13:05:40.718Z" }, ] [[package]] @@ -2621,15 +2709,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/24/8d99982f0aa9c1cd82073c6232b54a0dbe6797c7d63c0583a6c68ee3ddf2/litestar_htmx-0.5.0-py3-none-any.whl", hash = "sha256:92833aa47e0d0e868d2a7dbfab75261f124f4b83d4f9ad12b57b9a68f86c50e6", size = 9970, upload-time = "2025-06-11T21:19:44.465Z" }, ] -[[package]] -name = "markdown" -version = "3.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/32/0b4d0297064244f04af4331b65de16237cc3cce07474171ee14222a28981/markdown-3.11.2.tar.gz", hash = "sha256:87bf5d90f2d2882e2c02fcda182c2800456b985bff38913867c49706481514b0", size = 491445, upload-time = "2026-10-19T19:41:42.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/66/bcab3b677b71ad286b4e607e3427499537c76e13ad2e22584ba7a7d9882c/markdown-3.11.2-py3-none-any.whl", hash = "sha256:f80390747a2a0ff2ba54da074ce4f862f72a6685f608cd2637b646a6d83993f5", size = 114266, upload-time = "2026-10-19T19:41:41.244Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -2771,104 +2850,24 @@ wheels = [ ] [[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/e0/1e8ed4e8dabdcba8cba5dbf4b8ce49bb3b51e750177155247abb5345d48a/mkdocs_autorefs-1.4.5.tar.gz", hash = "sha256:856f75b327b7717923ce06267094650689a510a748c48657f5333965950c2788", size = 59736, upload-time = "2026-10-22T15:03:55.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/cf/3dfc07ec910553bfb1e5c9009c1854bd9d2efa7f3dda03f007a77db0e63a/mkdocs_autorefs-1.4.5-py3-none-any.whl", hash = "sha256:68ee484e0800f3ba50cda1bc4a94293adbdfcc010243c322b3f5d66e15d06770", size = 29633, upload-time = "2026-10-22T15:03:54.091Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.2" +name = "mdit-py-plugins" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "1.0.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, + { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/cc/e3ed8b16851f5d664dffcf8f55fc60d0d3f736a3edca21d1508105d1255a/mkdocstrings-1.0.8.tar.gz", hash = "sha256:b50fc326230906425c82ee9ec59b80e5db00892b2b593d4d0b841a0d4acf537b", size = 107444, upload-time = "2026-10-19T15:43:02.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/5e/4df7a2290e97c60a3ba410084966e5ab84aa89d15c8c98c195acbaefa8f4/mkdocstrings-1.0.8-py3-none-any.whl", hash = "sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c", size = 42209, upload-time = "2026-10-19T15:43:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] -name = "mkdocstrings-python" -version = "2.0.10" +name = "mdurl" +version = "0.1.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffelib" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/43/4783d5606de4f5e4bea10388c8741b0c40d03e7cb9527b94ad35966351f5/mkdocstrings_python-2.0.10.tar.gz", hash = "sha256:5e34ae56b2f20a57807ee479d603ddc5ec7cc6d2252c4c177754627ef2d3b833", size = 207682, upload-time = "2026-10-19T15:58:07.934Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/1b/fc924a97a237adda47f15c1b0cc271639e6b181ab3f95500fbf060c79a69/mkdocstrings_python-2.0.10-py3-none-any.whl", hash = "sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062", size = 105538, upload-time = "2026-10-19T15:58:06.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -3467,6 +3466,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/dd/b3250826c29cee7816de4409a2fe5e469a68b9a89f6bfaa5eed74f05532c/mysql_connector_python-9.6.0-py2.py3-none-any.whl", hash = "sha256:44b0fb57207ebc6ae05b5b21b7968a9ed33b29187fe87b38951bad2a334d75d5", size = 480527, upload-time = "2026-02-10T12:04:36.176Z" }, ] +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + [[package]] name = "nodeenv" version = "1.11.0" @@ -3647,6 +3663,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/7f/c2d1b436b6e7cfebac140c2579a298344b85f2991a2ce5c3615cefb29400/numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f", size = 10883718, upload-time = "2026-10-10T20:05:28.547Z" }, ] +[[package]] +name = "numpydoc" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/2f/5e9bf8ed0a93587b8089242b4926dfd04d4b3b0b65f04345d219a1225f03/numpydoc-1.11.0.tar.gz", hash = "sha256:26db0adaac8087e98da05c86b11198defd962aef7c21f4dffd2703c8c6eb4187", size = 101575, upload-time = "2026-09-15T16:31:08.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e5/f5c38d4899cd8edd5b522071c5658202077d113c3225484d779cf257970e/numpydoc-1.11.0-py3-none-any.whl", hash = "sha256:493ad2ce0d8275ca5da484ec37dbdbbc1334c3a6968dffd10ec729b51753e15e", size = 71464, upload-time = "2026-09-15T16:31:07.521Z" }, +] + [[package]] name = "obstore" version = "0.12.0" @@ -3710,19 +3738,19 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.44.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.65b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -3730,9 +3758,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, ] [[package]] @@ -3751,29 +3779,29 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.44.0" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.65b0" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]] @@ -3897,7 +3925,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.1.0" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -3905,65 +3933,65 @@ dependencies = [ { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/b1/a10c0d1a7e3ce44eb42635771004aa503e3b9ddde0bd39c524dda65f7eff/pandas-3.1.0.tar.gz", hash = "sha256:ecb9b2d8a817b1e1ba1fdb90c0abe9eab31b4fdf2562b7a56b86ac66ace17e49", size = 5933421, upload-time = "2026-10-22T11:57:03.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f2/d8d89ee886df6d959d7b9f483a55a0e5a1d5b034f08c32f4343a524af802/pandas-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d53387f8bbff4e7aa567619d4034ffdb1836db1e768a482851b904bbeeed8ef9", size = 11595013, upload-time = "2026-10-22T11:54:48.33Z" }, - { url = "https://files.pythonhosted.org/packages/ec/73/e5243378792b79d3d08f9a6782a0e23ae65cc8c6d2f476b9d2825946b071/pandas-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d2ca3b380f0f6cc788780430eb23762619730c11d53989f3982ec96835bfb9e", size = 11159002, upload-time = "2026-10-22T11:54:51.011Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4a/1064a6ec84ae047379a5969f6b331bd8d57013f08b30976564780fb0c9bd/pandas-3.1.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:388fad0c683ad061baabe23a76f43bf43c1d2c2fd46572b687082cfa195b7275", size = 11758276, upload-time = "2026-10-22T11:54:53.367Z" }, - { url = "https://files.pythonhosted.org/packages/14/41/2d264868728f7cb2add91a605bea5f2aabd3da41889ee97746a586ec34ee/pandas-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a28602573f50453b57802d282f9f8cb7663aa10201127ecbbe95ed4a41c64ce", size = 12330371, upload-time = "2026-10-22T11:54:55.654Z" }, - { url = "https://files.pythonhosted.org/packages/7e/04/790ac6114ceb45575d7d7a909126210f829978f18f4f10ea8c6c847f0118/pandas-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e6f83e44846f9386e3593965bac017a54a5b5c611ea7b836ceab7f465bb4cb24", size = 12777723, upload-time = "2026-10-22T11:54:58.08Z" }, - { url = "https://files.pythonhosted.org/packages/56/b1/0513aaaac23bc1400b831881448665565a1b9b327d3eb7c5aca2b23c0b84/pandas-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebbc0e652844e52e06d1525e5084ec1609158c086c8646aa6ccb34802102e533", size = 13411658, upload-time = "2026-10-22T11:55:00.372Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1a/e33f6637445dd4b7323aa7bdaf4cc15c383805b4a2e99f958b7e32296e58/pandas-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7704e3d7367cfae71d3f8c95ee81e178c7a90bef8d6eac30df9815010722259", size = 10977787, upload-time = "2026-10-22T11:55:02.967Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f7/86bd686a20a5052e15581fdc7f220098426b1982604fb545e78bdd5b5b2e/pandas-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:d3e660af30a6683fa57a5a2969555df117e873eaaeb418ad3b2e59249405ee70", size = 10305708, upload-time = "2026-10-22T11:55:05.046Z" }, - { url = "https://files.pythonhosted.org/packages/e9/12/b94c28870f351e9650c724e6daedf661455b348a9dca6d717c42c30fa216/pandas-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4772320b7735d96dbbef437129f840dec4175f8a18a2391bd1222ed8943825c", size = 11587014, upload-time = "2026-10-22T11:55:07.208Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4b/af8ca15261db52ab07f5d0c2d4c1bc28d822ea4c2a79756bc2606c012e7c/pandas-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc5e4e12d287ea77bbf4c4d4bf47ed152d82a990d3a6794dee7c350f887532b5", size = 11167712, upload-time = "2026-10-22T11:55:09.523Z" }, - { url = "https://files.pythonhosted.org/packages/49/c4/cd5127751e2bef3d10f07e0d856e120916a3df0df6450f759ddb48c78b1c/pandas-3.1.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b4e696ff70ed06e986feeec09356c491ce878d0df69a36c8e591b6a38afa29b", size = 11476964, upload-time = "2026-10-22T11:55:11.889Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a6/dc89cdd95e12093bc813e9b814a09f1a056be3e1304edaef9bf9dbee108a/pandas-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9433b5dc2f26ef9d9b56daaf4d41d078bf4372c5d88c42ed5b397a6eea80de16", size = 12019688, upload-time = "2026-10-22T11:55:14.246Z" }, - { url = "https://files.pythonhosted.org/packages/36/87/da14aa671da792612f15847451e731d5dc0f06f6747b76b15f197ffcce13/pandas-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cba20fd269808e224d6212273008173978033a1dee0f3f217bfdbd58775a72c", size = 12490149, upload-time = "2026-10-22T11:55:16.675Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a0/1ce996c7354ad37983955571058ba0dde585663163ab067275197183868f/pandas-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:743554ed96755ad1963397ccadbe9965d92db8777ca2ae8c1fe58bd9549d7f28", size = 13090624, upload-time = "2026-10-22T11:55:18.854Z" }, - { url = "https://files.pythonhosted.org/packages/92/72/d235f82b51a22478938a3583b68e8dc05811937543adf410f34bab76004a/pandas-3.1.0-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:ac2f833557874a79fc0b5fac4c86e402ea6498beba8753dc29ca23d08481689a", size = 8091628, upload-time = "2026-10-22T11:55:21.285Z" }, - { url = "https://files.pythonhosted.org/packages/10/72/06aefff8fe5efc5e1ca8bfb2695b648a435358f6dc224d4e40159314181a/pandas-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b8c75dbb27c7d170b25c13dd35515da4bb92d40fe8bb3bd2c69a95de2218889b", size = 10748836, upload-time = "2026-10-22T11:55:23.47Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b6/4c86f659f5b14790d14e17253becc93c4e3d1511b73f0ba2184f06b164f8/pandas-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:73b6cea22090b7123c78ee662a5108ff36616b0bec4fe47026f86fb04a49bce7", size = 10128898, upload-time = "2026-10-22T11:55:25.995Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/eb85f8b2ce121c21286842cf81abaa670ec3570173c512298f91128927a5/pandas-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:849d5019dead65944875ce21fbf71a2ca18e982164cd7367623cbda8567c3122", size = 11609893, upload-time = "2026-10-22T11:55:28.592Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/b327e814327b7e7e61b5c5cceafd5c0c20f0d38e94f99ebf2dd6c1cec85b/pandas-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c1e084760f3b275eed5863407d025b933d25f3247515fcc84bf664405e8bb3d2", size = 11182834, upload-time = "2026-10-22T11:55:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6d/2b918a45ad31d85f898567c948f11d0789ffe7fbadd6f487565e99d179b6/pandas-3.1.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1009f5b6819b81319cf177b4f01a2823ed54c81db8bd11cdf2375f42d064c02", size = 11421478, upload-time = "2026-10-22T11:55:33.257Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6f/b63e1d43eef5746014ae3f4786c72e0020e61b6b373a2abb6ebf1be3df49/pandas-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de6d0fc96e8551b83f400bf212611a0b3a2b389594d20a2540dacc61b21e7bce", size = 11991052, upload-time = "2026-10-22T11:55:35.556Z" }, - { url = "https://files.pythonhosted.org/packages/fd/43/2cf85145d822957a3f8959fd0a850e6bf687dac2823c612b3912b8888dae/pandas-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5611344f9f0ed51619ac7acdbf72b86f5043801a66e3f8730c8971257082e8ad", size = 12455560, upload-time = "2026-10-22T11:55:37.656Z" }, - { url = "https://files.pythonhosted.org/packages/7f/72/0cb19cea2e6afbb0854cbde0ed1345b61600e0de84eb7b73583fde3e3b55/pandas-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:39c67ed9e56ad3ac0727dc2ae6a1eef766988c762d5ce2e2a162613d81c8990b", size = 13045764, upload-time = "2026-10-22T11:55:39.969Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5c/ea5472e8444f5871427f3a6818725684fcdce74223ca1b22aa7ca345d490/pandas-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:07807035ad66de0d833d68f9bee05801dbf08b88d56a233b0e9fd189372c3261", size = 10729719, upload-time = "2026-10-22T11:55:42.008Z" }, - { url = "https://files.pythonhosted.org/packages/45/f7/8cdd350c67bc62865d1e30cd7add43dd631420223487ba42dfcfd0d0e052/pandas-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:54fed4a50b8321ff8d413b46b7e31a023cecefd06c7210c02aa980a9836428b2", size = 10116651, upload-time = "2026-10-22T11:55:44.367Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a9/e82d44fe20dc61dd2e15bdd17c78aa0023d860d55620e4fe82f9e644a089/pandas-3.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a8c44d40cbb6d00128bf2d8f8bb66c8d561744b6d44686b5a1a01f3b3fd1245f", size = 11675868, upload-time = "2026-10-22T11:55:46.617Z" }, - { url = "https://files.pythonhosted.org/packages/83/38/6be618d98e2cbbb99a40349717532f0c2b6794ddc62f1199e3f83e45a40f/pandas-3.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89b66823678cdd53cf97187e7cd6861402bdd147d11a383706093136a4e83f45", size = 11262553, upload-time = "2026-10-22T11:55:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/5c/66e27fb0d691e835bd55b336bdf47312a541a66a17ba4d72a988d743bb79/pandas-3.1.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a8dc7780720b9e2182799ee86279d4cc7d1fd30b230ea1b202da5b6f7e14c3a", size = 11548155, upload-time = "2026-10-22T11:55:51.674Z" }, - { url = "https://files.pythonhosted.org/packages/37/1e/42e81484c5a6791ab1b8c52c1ff1ef0483c119f710ed58ee603d241b8792/pandas-3.1.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4455cdcc03e42c17233184e8e4ea65c3ed2925d8b1d9d7dd8e6fced3f6e43c1", size = 12029136, upload-time = "2026-10-22T11:55:53.823Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c9/a3b4fd156457f3eb2585838cd56427a113eeeba679204de435a3224b8cc3/pandas-3.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:224a9267e5fa70c16bf4f8a317435eef98387940ce6c8c611f1f68f2c3415e10", size = 12575350, upload-time = "2026-10-22T11:55:56.006Z" }, - { url = "https://files.pythonhosted.org/packages/4e/25/23a017003d792117b297bbd87e35a1bb5658509f610ecd5aeaebb84762aa/pandas-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c261c7a808982557c2917d40ffd27916769f26aacb4e6775c01e4467bcd34c3b", size = 13096142, upload-time = "2026-10-22T11:55:58.434Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d2/f4074b7d1234d112aaa23a796be1574441a9fa873707eb98312a237a6185/pandas-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:e77112cfcabb3bef7281a72170670db9a5b31409a1cd3199939abb1fba67a7ad", size = 10885958, upload-time = "2026-10-22T11:56:00.895Z" }, - { url = "https://files.pythonhosted.org/packages/25/9f/bcfc9e0ea07f5c7583e129e62cd0529747317977ac1b85878b80235bb322/pandas-3.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:6c51273b89784c9d54020ed2f60e20524169aef349bddf6cba9f5e83b46077a7", size = 10281257, upload-time = "2026-10-22T11:56:03.478Z" }, - { url = "https://files.pythonhosted.org/packages/05/e4/b3bf0b702c0a886e4abcfc2ebdc681310498592a10d9ca0ce5db6df47b70/pandas-3.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84377fa82d0b40165379a0058d8a1a267ceb09e65160de6fcc75475d4d595884", size = 12134028, upload-time = "2026-10-22T11:56:05.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ff/9e08cf048d73f4dfdb10da859790cd1a4213465dec39ea367a606d580c4a/pandas-3.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:37f88576f3449bfd86c1052f6b42274b61d7250c8d9dcc55f9dc4de7fe9883ae", size = 11729164, upload-time = "2026-10-22T11:56:08.335Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ff/a0df60d70fac10006841457c30d35647ae0c93af33e420e5e047b15e058d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:756fd67995f3efb6b25f8eaea95c0e893f84e0068e2c53c5fdbb65275041d4db", size = 11413041, upload-time = "2026-10-22T11:56:11.38Z" }, - { url = "https://files.pythonhosted.org/packages/55/04/e6742d85ba7d166a1a8a99ff9f4aa9e585fb24028522daa187859ce50b5d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d9252b57bded194d09cd5ebf18d9b4edac238b16f56984ea8281d2ecfe39cd", size = 11869427, upload-time = "2026-10-22T11:56:13.637Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a1/c9f4915408ef10b541f31b7812ca038dd903572c163b7be7bdcece8a5110/pandas-3.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:461b79db058639e446ddf90dd7f4804fae385acf0669f6e31321333e14cb6bc5", size = 12430367, upload-time = "2026-10-22T11:56:15.829Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/636b9cc8d30b3e2027b43036dd5a8e9e7cfdd5aa86f4454235c5ef7f96db/pandas-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bfac077874a396b7ae61a307b27aa6f0256cf31c143b7f96ac622765754cc300", size = 12945204, upload-time = "2026-10-22T11:56:18.419Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5e/5c28942fa8c6cf2d8b4e941abee43a44ba0a123f9fef6411f0319aefddd5/pandas-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1340f823a1d189fae8365de314c4d3c2ea0173c61bca66cc76a91385c64d9d3d", size = 11297553, upload-time = "2026-10-22T11:56:20.934Z" }, - { url = "https://files.pythonhosted.org/packages/cd/a4/5991dfb2cccd233ae754f2fb37a4b1474d90595d83f6790467dc5a7d8041/pandas-3.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2bb3f8cc42fae0c0de5186778f4a4957656b9b3955aca48383abfdac44a83598", size = 10626068, upload-time = "2026-10-22T11:56:23.187Z" }, - { url = "https://files.pythonhosted.org/packages/77/26/5cd42609d8e6cb3991242116a0e499d09f29f5bcf574f72f0a4aba13a87e/pandas-3.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:04ca88727dfa86837030c493d7512bb1c008aa4f5f14149c5f8d3ff446791340", size = 11666949, upload-time = "2026-10-22T11:56:25.842Z" }, - { url = "https://files.pythonhosted.org/packages/97/5f/ca1ac99da700c215c52fb0f5063a7fbf14878c6e100f687704c21985640e/pandas-3.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c0bfec4d188fb0c05bba47b9f9def87624b8c545913aa3585a28f19f27ce847e", size = 11255062, upload-time = "2026-10-22T11:56:28.201Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8d7aef062e7d1f48869288e5c5ae5d109923f37ed3bbd0fdcaecc87784e9/pandas-3.1.0-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfb087db83eb79ca348047961d367bdd4bf9820d90a5945f3652c7060da7363", size = 11547558, upload-time = "2026-10-22T11:56:30.492Z" }, - { url = "https://files.pythonhosted.org/packages/06/c4/a041c786bc29935afe44806f72c0111f2643cbac5cab761007a227e12e97/pandas-3.1.0-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b5ce18612ca955e7a95d0bacc20120aedf97e2fbbebbe4de029fc24479e79b8", size = 12026259, upload-time = "2026-10-22T11:56:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/d3/4a/ce6690c7e6081c96857ad7dcbe3b978394d8db682610755919e61863516f/pandas-3.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a13199463b129363701a540c7b79586581b246b10b6a7ad682c41b268e7991d5", size = 12563821, upload-time = "2026-10-22T11:56:35.402Z" }, - { url = "https://files.pythonhosted.org/packages/75/ec/ae440ecbfa8f5ba0230541b4519c1fed0c4293ff362f18f7f353b12e2dca/pandas-3.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8e4de3fe8017e3445f1174707cd977e4e1154a704af2c060a1f648862f81acb9", size = 13086426, upload-time = "2026-10-22T11:56:37.666Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1e/6459d4b0ed9c3d72b9ab63387ec11e1e658579e702e0ef117be8b5d7d0d1/pandas-3.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:eee72bba8a72f0bf9522132e5c87cd10fce9d77440b50f65326b093ec9fb8d43", size = 10877850, upload-time = "2026-10-22T11:56:39.994Z" }, - { url = "https://files.pythonhosted.org/packages/f5/22/e5fad1828e5f923e80a8d7ba9187f687159004a10f99f95dd5346b04c54e/pandas-3.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:9ea2983fe096a3d997c8c4b9f26775fa51e7fb251b10634bd7f87e6fe3542d52", size = 10276384, upload-time = "2026-10-22T11:56:42.193Z" }, - { url = "https://files.pythonhosted.org/packages/59/76/89820c2e1950425f39d30695d2296a8c7d400c4c2d279e7618363a7289ac/pandas-3.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:84d73e0814a38711e4f2a24b272f748a0b4335169d2f9752407ae26bde22990f", size = 12107257, upload-time = "2026-10-22T11:56:44.498Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2c/af4397e6e855d2c0dcb3392ab7788087be4ef53f6a3f166daad8b83c783e/pandas-3.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d3920d9b4de89fbef76ba6b863d58a330b5bfb720c9f8f58eb9b7da9b711d034", size = 11704430, upload-time = "2026-10-22T11:56:46.791Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9e/7791cd269099f9fca7dc4718955cfe4bb94aedb6634b59eeffefb1ed6a74/pandas-3.1.0-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1d61e0eae5219cd024fcbc21304de759964b5642ddcdbb28d9a8ba442ab6901", size = 11378442, upload-time = "2026-10-22T11:56:49.113Z" }, - { url = "https://files.pythonhosted.org/packages/54/6c/da4013bd33c49a12eb3127771111caeed3222a985651e3465bc06f839ce7/pandas-3.1.0-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbb121205af7bc5e587b94a07b5f67b5ba687a918665b9e8748f21388a88b044", size = 11850193, upload-time = "2026-10-22T11:56:51.558Z" }, - { url = "https://files.pythonhosted.org/packages/14/89/8c820bd6fb6ad8f0edb2877639bc7e164bfb4b8e4cdf7a2202f54959ea1b/pandas-3.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:684ef3add5a92abb99c1622f2bd04b54b8843075a266e8240106ca94ce1d1237", size = 12394022, upload-time = "2026-10-22T11:56:53.785Z" }, - { url = "https://files.pythonhosted.org/packages/92/f5/9d753d85c4ea85b6d1c3735496ae5b416d7ef6905f1845c70e9521d124d9/pandas-3.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dde9a55ecd3c4f9100ac50ab15e852e4c54fcf372d800e4495e0b2159a0122ab", size = 12916085, upload-time = "2026-10-22T11:56:56.084Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f9/4a6965677477dbbb19f8881befb01b67eb6009809db73f7f1b7a0132faf1/pandas-3.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:3fe234bfb9999552dc54c45851752e56e163218d7663897dca950b4a7c32af39", size = 11267097, upload-time = "2026-10-22T11:56:58.479Z" }, - { url = "https://files.pythonhosted.org/packages/99/35/e60fd8228db4eb848802988dbc6e3a0f1a81c57f06ad95ebe4cd534fa655/pandas-3.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f1a93c24175129bfc9922ef748ddae1d8137c743ed835e2615be329f606fed4d", size = 10597861, upload-time = "2026-10-22T11:57:00.776Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/48/88e8d250d28efa8163294f6809a71683c7ee67f63ac7c33021c0503b3547/pandas-3.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586", size = 10391798, upload-time = "2026-09-17T23:20:20.96Z" }, + { url = "https://files.pythonhosted.org/packages/55/a6/39db5d41f3eb5d7846626312cb30e0cea48e988fc47a3725698bc931ad3c/pandas-3.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af", size = 10023718, upload-time = "2026-09-17T23:20:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/b7/707e966129f77ee8a39d41a41bbd989b790b3fef581c6e26b821315cba6b/pandas-3.0.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808", size = 10612800, upload-time = "2026-09-17T23:20:27.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/be/dfb6cc9329d0bbe76dadda8a1113d026bb996ec76c509221368dc68349f4/pandas-3.0.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258", size = 11108900, upload-time = "2026-09-17T23:20:30.65Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/3d58f15bbe972f3d7bfa13ee06d731785a76fe8d232c92b2655a8de14127/pandas-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b", size = 11630226, upload-time = "2026-09-17T23:20:33.582Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/9b494de4a3dd92fc6eb19187db1b21afb50fdc21962f32ea1aca9f270cb2/pandas-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3", size = 12146942, upload-time = "2026-09-17T23:20:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/d2df02854aec5d47659acfb2be352eecc691845b2f86e99c84f1010a8671/pandas-3.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd", size = 9859246, upload-time = "2026-09-17T23:20:43.976Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/2a30df0d7dede5c195300a1820b0bc21cea3aa24e4c8b6c4431565ecc79a/pandas-3.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171", size = 9106923, upload-time = "2026-09-17T23:20:46.611Z" }, + { url = "https://files.pythonhosted.org/packages/77/4c/597d588c055d4373cff19cbbc32d4dd046c7be8fadee957585b5ba9e5b24/pandas-3.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7", size = 10397411, upload-time = "2026-09-17T23:20:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/48907c7c707b61a8e5018c32e1a3f70623209bfb59020a2f6196159d3aa7/pandas-3.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172", size = 10051279, upload-time = "2026-09-17T23:20:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/67/fa/613d867c3d9554a61bafdec6f79565c8a3e73235feb52cc4a72ad2e0fa6a/pandas-3.0.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281", size = 10303429, upload-time = "2026-09-17T23:20:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/67/0c0f18e38d7f2d2af8c24b3315bc4046e73bbdd4a5540405506671ad0c0d/pandas-3.0.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d", size = 10788193, upload-time = "2026-09-17T23:20:58.617Z" }, + { url = "https://files.pythonhosted.org/packages/39/53/1b57f3162501fe36687e4870e1b918a6458ca7386b173af75663ace95857/pandas-3.0.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b", size = 11323501, upload-time = "2026-09-17T23:21:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/b1182e8d39100369d7f13f4a125a3fb6b096fef112c46d0566c25781ff68/pandas-3.0.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c", size = 11838173, upload-time = "2026-09-17T23:21:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5b717af24d2f27995e51e0875a269dddd373045216e34cf62e3aa764eaa1/pandas-3.0.6-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf", size = 7118499, upload-time = "2026-09-17T23:21:07.661Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/14b3b17cd75cef4a1ee1af4234eb41cc1afe4103b98c2d80e8916abfd42b/pandas-3.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b", size = 9661439, upload-time = "2026-09-17T23:21:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/3b/11/3d580a604a1e35d69f6676847bd12db7d14344bc677b717f4413e79c5d0d/pandas-3.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b", size = 8959806, upload-time = "2026-09-17T23:21:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/143605a1f6443ad50ebda78a31e5a3a10147fec2590e931584aaa5ff0a09/pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2", size = 10418900, upload-time = "2026-09-17T23:21:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/87f8548f73d452aab35e4a90f8b39ae303295e0f2ef0b4055c44d6b3f1be/pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa", size = 10064785, upload-time = "2026-09-17T23:21:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/d951442e5607c6e3b2462eff8f420797d428aa74b87c6ecfe4f48553626e/pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c", size = 10245290, upload-time = "2026-09-17T23:21:22.797Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/96d50e1e6cd0b08b5e2b7c838f65ae644940f75a124063380b5ef73b6866/pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658", size = 10757657, upload-time = "2026-09-17T23:21:25.673Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/f82d13a2cb703e1a8acee7e01fdc2b898d9cd0c00f07d1dfce63af43e350/pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2", size = 11249114, upload-time = "2026-09-17T23:21:28.898Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ce/8aef2e561a2f2c8b38c913c67373c65ba6748174e763d27c80271b24bd17/pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d", size = 11820511, upload-time = "2026-09-17T23:21:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/63cb67e6903ef6d9c2871916dbcbc09d254da0fe8b870cf62e16b21945f2/pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd", size = 9638092, upload-time = "2026-09-17T23:21:34.883Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/e7b35b712edb068d382ddc8b2bea8a04974100515ba2daa22b478b265842/pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f", size = 8952032, upload-time = "2026-09-17T23:21:37.729Z" }, + { url = "https://files.pythonhosted.org/packages/75/55/1a8875395b05ccd572cbca0b9255dcd2db6e6508e632a558c1a6884b39ad/pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1", size = 10492213, upload-time = "2026-09-17T23:21:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/47ae13476995cc8a40cd609e93e7cf11f273d8692925c2903cb6d38aa0d1/pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729", size = 10156618, upload-time = "2026-09-17T23:21:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f2/cc5f2adb8d6e86a85d9fb5128f8cf205a61189336f70d1f7faf0d1b53ec9/pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34", size = 10375479, upload-time = "2026-09-17T23:21:47.159Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/ffdcb19be4ff6bfe7d969e7cef2c567c633df5a3a1cc1053394ad053bca8/pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1", size = 10783244, upload-time = "2026-09-17T23:21:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/e150075b2c6eb69fae896f2d9239bc6ed07db97735971d53d66de6553460/pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de", size = 11382223, upload-time = "2026-09-17T23:21:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/b441c587dc7355bf6e1f68a91b4f76a6c29740f0c23be3acc5d4ebbeea6d/pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c", size = 11854881, upload-time = "2026-09-17T23:21:56.342Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e9/f43410fada510b43fec09993c08f552086c3d247d3ee801a678f3cb10ea5/pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553", size = 9791672, upload-time = "2026-09-17T23:21:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/db14c059c21f9baa1907d436f8bf30e0c76c6288225c5e8b79a08ba8b2c5/pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c", size = 9121246, upload-time = "2026-09-17T23:22:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/67/ba/bad0f8dac020ab38a8637fddab01a57a82da7a496a6e6f19590aad53ab62/pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514", size = 10920612, upload-time = "2026-09-17T23:22:05.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/b500982e9aac6d52a58da4ad3f11e14168a315b06906b3f397c427878065/pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60", size = 10570213, upload-time = "2026-09-17T23:22:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/e6ecd0073c98be8f840ac3125b955272835d7d9fd69f5944383b164deb5e/pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541", size = 10252356, upload-time = "2026-09-17T23:22:11.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/f5/001e230a7a7803590d9275a1a3f7e1bb605e3a495cfe5e8d3a532090621b/pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965", size = 10655952, upload-time = "2026-09-17T23:22:14.283Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/bab587148a3852c96aae26c4b5f9e04ce2221801ad94e166b4fbf969ede0/pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7", size = 11274177, upload-time = "2026-09-17T23:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/74b48d87df2b80892d713c149abfe36d5db4de41b4eccb042a2bc07dafc1/pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44", size = 11719041, upload-time = "2026-09-17T23:22:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c6/d64b72d64d7eb0fad9fe424d34138e45dee70ddbea1360dcd0adf30e28f6/pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630", size = 10191388, upload-time = "2026-09-17T23:22:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/5e5b2ccabeca73ae2b03fc82bca3eabb7466cf43737f05ac08d591665d47/pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a", size = 9387796, upload-time = "2026-09-17T23:22:26.64Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/47c5fb0be8bdd00116814c2c40d9ec42dbeb943865ef95130fe58a898226/pandas-3.0.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570", size = 10483960, upload-time = "2026-09-17T23:22:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/09/08/a310cb2fefe6d2b4623ab2150da818d93d4e73e33b58e4d163bb74243c5a/pandas-3.0.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34", size = 10149432, upload-time = "2026-09-17T23:22:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/36/6af478ec3a26d7754555cd62c3101c589c0931b1d85398e4fa5403910a1c/pandas-3.0.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e", size = 10360981, upload-time = "2026-09-17T23:22:35.621Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/5ece0e9cb79a6473216620db6a90546f578001c2bd857b77c444b27acad1/pandas-3.0.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c", size = 10776585, upload-time = "2026-09-17T23:22:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b6/cd3038f31ade5e8d2b4e1c9549d4b31e4d598469562f47142b2ad9171c0a/pandas-3.0.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19", size = 11385249, upload-time = "2026-09-17T23:22:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/05bb3003737f80375d7311774916a24d161e2e591abe8c672aed7813defc/pandas-3.0.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e", size = 11848595, upload-time = "2026-09-17T23:22:44.207Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/1c0d46acf236d19ef975e9cdd5d1e0dd54924b03f0ed8287096ad4a18152/pandas-3.0.6-cp315-cp315-win_amd64.whl", hash = "sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de", size = 9783810, upload-time = "2026-09-17T23:22:47.097Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/df3304a9c2833c4810e7f1c887b04105b24731f9deef8b5d5d04522a375b/pandas-3.0.6-cp315-cp315-win_arm64.whl", hash = "sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7", size = 9116820, upload-time = "2026-09-17T23:22:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/47/25/d5f6cce5efa17c38e4f752a875b4a26d66cfadd529cb8c81672f0376d6a6/pandas-3.0.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640", size = 10894614, upload-time = "2026-09-17T23:22:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/e1876bfdc1df06bafc022d33202b5663bd86c81df2a6140aacafd0344669/pandas-3.0.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36", size = 10546973, upload-time = "2026-09-17T23:22:56.155Z" }, + { url = "https://files.pythonhosted.org/packages/e9/27/e0a27a5a5c27f7db66657b44def9e93121fd0ad4f0808355b9898fcbf204/pandas-3.0.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804", size = 10239717, upload-time = "2026-09-17T23:22:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/4eadb7d86a921c1e8bc70916cfe601ed667c4169118d17d69958611c21f8/pandas-3.0.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e", size = 10641275, upload-time = "2026-09-17T23:23:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/eba72e9d905e84e79aefeefdcc6bc1abe15d9077973566643f4211636966/pandas-3.0.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266", size = 11262919, upload-time = "2026-09-17T23:23:06.038Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/1c5bd2642b450e374b6191189f47c49538fe41f82348a66de6f647e6ab59/pandas-3.0.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947", size = 11709148, upload-time = "2026-09-17T23:23:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/73/3d/1b142bd0d0f1326a5d98c91c955b923cd1e06b5eecb428cd9d8817fa01c0/pandas-3.0.6-cp315-cp315t-win_amd64.whl", hash = "sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a", size = 10161518, upload-time = "2026-09-17T23:23:12.365Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/6419c14da2adc1f09a6a183b8f91d7494d325b287f4ca984ac04f663638a/pandas-3.0.6-cp315-cp315t-win_arm64.whl", hash = "sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0", size = 9364529, upload-time = "2026-09-17T23:23:15.274Z" }, ] [[package]] @@ -3998,15 +4026,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/8d/a9c2a531da0ebb54b4a7174450e8534a39db112a141ae3a437de28420111/pgvector-0.5.1-py3-none-any.whl", hash = "sha256:ec5bcd5ffaefe6ecb2dcc9564ca921d284564b969183bc837a144604773af8ea", size = 31056, upload-time = "2026-10-09T01:50:21.614Z" }, ] -[[package]] -name = "platformdirs" -version = "4.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, -] - [[package]] name = "pluggy" version = "1.7.0" @@ -4059,19 +4078,19 @@ wheels = [ [[package]] name = "prek" -version = "0.5.7" +version = "0.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/0c/985c6f0fe1671e639fb97bf5a60f3945369f26d4ccf5a8a1b063d357e6da/prek-0.5.7.tar.gz", hash = "sha256:91049700f0f13b4575deb44045dd6dd51f7c4a70c53ec5d452b09925962e3b35", size = 580753, upload-time = "2026-10-21T00:46:48.231Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz", hash = "sha256:16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a", size = 570428, upload-time = "2026-10-05T06:08:59.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/36/56c107d100cabfb7e7c7f2e62d363052c77c3fcf664707b66ef9e3b865a4/prek-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:621835811891d11d3feb5ca47102ca97973c5b4e6fbd1b35b1293d412d29d9cc", size = 6062121, upload-time = "2026-10-21T00:46:31.418Z" }, - { url = "https://files.pythonhosted.org/packages/51/59/1868e2d6b3581eb9f327cb7db4270c4f38e80c5b175517e439e106eac704/prek-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ef10cf46c0660b011fc067b7ce20dc78f2c44b6f905bf8dc49402353f67283b1", size = 5686422, upload-time = "2026-10-21T00:46:34.006Z" }, - { url = "https://files.pythonhosted.org/packages/7f/e3/90901432ee7ca288b46872552ca8f658b2262cf56d9af3c1eb0416f206da/prek-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b38075f9363066d72d1cd887b5a260ec938516fea9ea44def53e592ee75090d5", size = 5817816, upload-time = "2026-10-21T00:46:35.869Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d1/f8e24b15906e433850a2c8409ae3a9270724b2a5d4a71b8c4eadafeb201b/prek-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a8f7734931c4b6a5f2efca121fabed7979408df233ce6d9b043f4148873021c", size = 6276177, upload-time = "2026-10-21T00:46:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/4b/15/c83456872211706839ff326474aa9f2d870eef7ee5ec2c436446519779d2/prek-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c139c494fb1d9826688f8107d1514fa2a58f8f4a0f0527b68e7970fa50a8b723", size = 6140221, upload-time = "2026-10-21T00:46:39.29Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b5/996b05447d4c2a4399947a7c5c910b2cd2c544b3074c11153ae3e21b4112/prek-0.5.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e86139ab5662124818dc1957854d8281f635414016e545ab72e63365c995824b", size = 5812638, upload-time = "2026-10-21T00:46:41.006Z" }, - { url = "https://files.pythonhosted.org/packages/83/e5/60e0e76cd6819d75026b3a8147ddbd2b3fcab27c02236bce8b3a559e0e64/prek-0.5.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:94bd6a93ccf8ebb1d30b822161515e3e31ea916ce7411febe8ceb5f607eb840d", size = 6298681, upload-time = "2026-10-21T00:46:42.618Z" }, - { url = "https://files.pythonhosted.org/packages/54/97/e14b12e7d8fbdb468581d778fb892c855fbca325d4344cc535c12e2851a5/prek-0.5.7-py3-none-win_amd64.whl", hash = "sha256:fa69837b6d1d2b6be5fe7fe4cd27fd5606f2155e784fec55ee7df7ed9f9bf211", size = 5692294, upload-time = "2026-10-21T00:46:44.385Z" }, - { url = "https://files.pythonhosted.org/packages/bf/28/9dc15c0644cc6019c16c5cdd44e5004b641f5e910488f830e80dfdd11042/prek-0.5.7-py3-none-win_arm64.whl", hash = "sha256:239ccb081e3766d83a6a9a5ebee1d5cbb7986fc8e4f230e785423d409cff95e3", size = 5626111, upload-time = "2026-10-21T00:46:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75", size = 6038669, upload-time = "2026-10-05T06:08:39.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e", size = 5548955, upload-time = "2026-10-05T06:08:41.428Z" }, + { url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b", size = 5848228, upload-time = "2026-10-05T06:08:44.051Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f9/1279e1c83819de5904db1a09f4afc589932a2e62c7063713b1fe3a54ee46/prek-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f6c453a12c21757b23bbbc97415aef78527841ad3f9e61f52cb35cf012f93e5", size = 6371296, upload-time = "2026-10-05T06:08:46.278Z" }, + { url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850", size = 6269922, upload-time = "2026-10-05T06:08:48.645Z" }, + { url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99", size = 5861518, upload-time = "2026-10-05T06:08:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/b7/03/690fba9543033e25ea2e7b2ae1cca1ae8d8c2e32295f0d66f4b50934b8c7/prek-0.5.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c69f60c2b3b4d481e38d0eab3b97ac05ec05d7648ae690925ae95ca9d22c7643", size = 6383685, upload-time = "2026-10-05T06:08:52.868Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl", hash = "sha256:15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874", size = 5784759, upload-time = "2026-10-05T06:08:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6", size = 5517355, upload-time = "2026-10-05T06:08:57.193Z" }, ] [[package]] @@ -4543,11 +4562,11 @@ wheels = [ [[package]] name = "pycparser" -version = "3.12" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, ] [[package]] @@ -4713,7 +4732,7 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.16.1" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -4721,9 +4740,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/ca/7817b4bf0c4b15c6a6ab8cdf4cb96c268335a8ca2f525de93b6ebe4c7d28/pydantic_settings-2.16.1.tar.gz", hash = "sha256:aff602a1886338c2df3d1e958e2193a8dc33cec75b91f202e83fd98484b4b0da", size = 303436, upload-time = "2026-10-20T11:37:19.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/3b/a5d2294799b53b448319978cfb5bd139d5a9d45e862af91661614f14c922/pydantic_settings-2.16.0.tar.gz", hash = "sha256:5b6c578049ede4db0e2ef3b4eaa4ad4069cfa9211f83fb38df899dfade50a614", size = 301031, upload-time = "2026-10-14T12:44:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/74/fdf926afe145d8629c4b2ff57f4ea1d64ed7abc4d84d97463b2517a2547e/pydantic_settings-2.16.1-py3-none-any.whl", hash = "sha256:30f5313340da4e8f1b440d1910b92a5bc3ccbb6b4ba70ec2b8395bc921f418f5", size = 80771, upload-time = "2026-10-20T11:37:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/b987bf8c51e5b19a95fa66d1ee596074141e085d9c2ddf97920803c7029b/pydantic_settings-2.16.0-py3-none-any.whl", hash = "sha256:7e73acf7f61936a15e5a3b6eedaea29f133357faf7272f2607ba479b049dd7f2", size = 80364, upload-time = "2026-10-14T12:44:08.233Z" }, ] [[package]] @@ -4735,6 +4754,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pygments-styles" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/2c/3886ed4783dd78bb62ccab7d43380f526a7e2ff0db8c77d9c87559b2f5de/pygments_styles-0.3.0.tar.gz", hash = "sha256:67746b8fc6ff72c1179ca4d9a8bc89c7f54c196c2ff9d087f07392cd6fde3ecf", size = 15258, upload-time = "2025-11-04T13:15:23.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl", hash = "sha256:c6c45e9939eb7590345bc9084113bac46c45f12b009d13422be02e80e84a034c", size = 36617, upload-time = "2025-11-04T13:15:21.989Z" }, +] + [[package]] name = "pyjwt" version = "2.15.1" @@ -4749,81 +4780,61 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pymdown-extensions" -version = "12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, -] - [[package]] name = "pymssql" -version = "2.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/f9/b56dfde2a0edf51a298b67ffaea62aa8c9459fb32c44b4f4701258debfd9/pymssql-2.4.5.tar.gz", hash = "sha256:ada0e747bab96b6b7ee0d21ca019db6e099cda0593080f59089e182a0a14b42a", size = 212797, upload-time = "2026-10-20T00:19:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/56/85067bd635017f8de62dd377db9f9966e8b6b725050808b271f0fab2b9f2/pymssql-2.4.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:29e9a463fc19a1029a33df49f29e2f736bf97e511de48e690864c4572a9f8979", size = 3213173, upload-time = "2026-10-20T00:18:07.039Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/a3dc723564ef6143d6a276188c234152758ba8bcd1799ad84246e12f04db/pymssql-2.4.5-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:d8c1fd28defbab9bb4144b86a319924f73bc24ccdfa715f76105fbac35035ef8", size = 3017602, upload-time = "2026-10-20T00:18:08.697Z" }, - { url = "https://files.pythonhosted.org/packages/30/95/769700844802c37cafb679d7f61092d82d5df5ae8ab18837e74e3a64d6c4/pymssql-2.4.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d6f83ba9e9095b6b27e3c72ea99c76b29c1d7f7011c77411b5b6fd3c18b5633", size = 3070534, upload-time = "2026-10-20T00:18:10.023Z" }, - { url = "https://files.pythonhosted.org/packages/33/3a/c7ec2db346f809c3cda1741e90e7f4b9af9ff4856f74dc34274908c9ecba/pymssql-2.4.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b428cdab437eb1cb3d8298bd4fcc5eb58c359c5915949f7f418374e29b79114d", size = 3249743, upload-time = "2026-10-20T00:18:11.358Z" }, - { url = "https://files.pythonhosted.org/packages/19/66/64fd381ee64a4c898a9957f0da8bcbd7d8b8dc871abd37fb30de7f0b8e85/pymssql-2.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a591f363ed0de2c84e988ce50150649b3c325e7609f11a240016db02f52a937", size = 4462361, upload-time = "2026-10-20T00:18:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/178f3864083f722fc4b9234c1008a290e917acede1556bb5a6136c028eed/pymssql-2.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45b1c3989d656ee806911474d9c1a20ced2970db6364bbf35046ea8599ddcb1d", size = 4199687, upload-time = "2026-10-20T00:18:14.228Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/284adab7f871d2ed4b93f6e02bbf87cf5c78dd6dfaa664f7df48a6441c1a/pymssql-2.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:56763b89ed308fc9e47644bbe8eba6ca19a18584b36cff53160394bc3878b0b9", size = 2328148, upload-time = "2026-10-20T00:18:15.902Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/87491f22bf656ced8adb59cdd614f744438b1460c94275eb42cab064faef/pymssql-2.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:984237bec08410e09c28b4fc380d48bd1c4b72d6de56dc80f3e8cffab5aa55ec", size = 1855894, upload-time = "2026-10-20T00:18:17.22Z" }, - { url = "https://files.pythonhosted.org/packages/59/bd/2bfd56916b902febfd9e797d698fa9a3672bf250582ffa8b4c53d1e25473/pymssql-2.4.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e316c829ba6cbec50a66fd37cc34a7c63a87777578bbb28e9de1519aad144d3c", size = 3195489, upload-time = "2026-10-20T00:18:18.42Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/40e7f691f66e5583ec46958129fab37a85a8d58edf62022c1be9fd564f89/pymssql-2.4.5-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:4cea4cd70bba994fdadc577c758fc18c43d22a672c01c5bf781f74c2545719ec", size = 2998450, upload-time = "2026-10-20T00:18:19.732Z" }, - { url = "https://files.pythonhosted.org/packages/c4/32/9308ebfe76c4ac9eedeeb0d49cbf449e8cf807b799497564dc0704f1fd02/pymssql-2.4.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1055176a5f1dda28d90ae7c10db7b3fa785c824e884ca1edb7d24d5a080b91ac", size = 3101331, upload-time = "2026-10-20T00:18:21.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/fc/358f9189f27ce60b07c1a525dbbffc20c944df09e60e116c48e0ec3e270f/pymssql-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cf2fdd1f626053b804928eec74418e2d5fd70de02e2bfa4ed04cf34e1759c99", size = 3279411, upload-time = "2026-10-20T00:18:22.422Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d9/6e6ef2f089096926f68f0905365fc611f3bd9715c17868393b16a028c1fb/pymssql-2.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:72a74d0a3ee2d5a5810b42896bd808d7b93027b3b5b44a245cf0f3a3d0ddfd28", size = 4491849, upload-time = "2026-10-20T00:18:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/d2c18276f8ea90f57ecf3a95681986847c5c5290b361704b578a06e92191/pymssql-2.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c390159a7d662401f6347d4c2210ef3ec8798f5e1628b6caa0bdd9bcedc5b65b", size = 4227456, upload-time = "2026-10-20T00:18:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/f06efea0e9072312fec21cabe1480743763278b9d8193bbbef962a1b614a/pymssql-2.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:e4a225585dac65c8e354a6cc12cdd69aa9c421d0ad2fd2feedc58d28af9b2923", size = 2315806, upload-time = "2026-10-20T00:18:27.2Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b2/5081c4e7bb13eca469da6259006def2f58d35744bde79884cd6139daffaa/pymssql-2.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:0358f787afa508237c3ed56ed9a6f0134aeed7aa8dafa1fceb43863c77b92ebc", size = 1839736, upload-time = "2026-10-20T00:18:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8f/c3dc712661c0cacba464c8fbaee865e5ebf39667f7a4c2ae773d6f7630c8/pymssql-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c154cd8d803265671136dba873206fcdb87b70b0c319b01ef095a173aead05", size = 3195529, upload-time = "2026-10-20T00:18:30.316Z" }, - { url = "https://files.pythonhosted.org/packages/26/2d/d53796f3de0b0fa32667c8ea547273930b099ed22dd3b2e427c8746ac91d/pymssql-2.4.5-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:81071a47a9f026c6b154c02a7a7b842f44c9d23ec461c8a0d4ecd9d09485808e", size = 3003695, upload-time = "2026-10-20T00:18:31.894Z" }, - { url = "https://files.pythonhosted.org/packages/c2/02/0a194ea53f44f60a9800cf9fe6acdfaaa264247de9ac26e63c2376a8e62a/pymssql-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ec1b6ca9d74f9d9280a84dfd7d2c7dea05651ea3108620b91dfaa4ec8d1bcf", size = 3100808, upload-time = "2026-10-20T00:18:33.445Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b8/af4fb12dc0e3ca21acc7686987a24d1f8cd8e3d69fb9a4f66729cab339db/pymssql-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ee37e118fbcddca6ba0d30e227c40cef5606c6ff182653304fb13de66e9883c", size = 3278947, upload-time = "2026-10-20T00:18:35.04Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b3/beceb56d905380fafddeb7466d22aff6ffb26603d8458256030a0cccf108/pymssql-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:441c85480d00c8e2d4931007739a5f87c030cc5f650ff531740a240a0ff529ff", size = 4491325, upload-time = "2026-10-20T00:18:36.514Z" }, - { url = "https://files.pythonhosted.org/packages/cc/ce/0984555c992a2695d01550282a88ef3951cc19b1cf3d5d1e8f0b501d6400/pymssql-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd434c8aad33b46a71509b8071e290e8c0a5cc16e061b4f16e1f7fc38c451034", size = 4227038, upload-time = "2026-10-20T00:18:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/ee/57/880e92954c2dba28ae12a5f1f05c1b7269df02aab35327a210ed00221de4/pymssql-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:4f6eb7a883ac0a81fc6bd62b5b855627c539070c7731683f00df80fd344372ee", size = 2315376, upload-time = "2026-10-20T00:18:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/6032ab12af2e7cb71adcc98fe0b4241f0fd4cf5ff94cded3756faebbe66b/pymssql-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:0122993af8d8ace54bff20f5b8e3c911b2cc5682331f9f952a5bd5d88f578ad9", size = 1839051, upload-time = "2026-10-20T00:18:41.284Z" }, - { url = "https://files.pythonhosted.org/packages/04/f6/39c90b800b19ad2073861dd1bc5fcbb255ac2651bee3d54dc74de4b20c5e/pymssql-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:515a5866a8feb0a3f8d518af6fd32240ce2d44d580e80027eb934d2d320b87d6", size = 3199285, upload-time = "2026-10-20T00:18:42.654Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b1/4144cb4b7034283b26b5035448983c0fead4c734176e4573de7288889ea2/pymssql-2.4.5-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:763b1a36b0aa230ffaf6625647fa289bee220a554a058876d2f308bc9945e725", size = 3005654, upload-time = "2026-10-20T00:18:44.269Z" }, - { url = "https://files.pythonhosted.org/packages/08/7d/848e48879f9152e758ea7f51f4ca89e81cb7a5809a538ee318051b129356/pymssql-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00665fc7457224c3ce0968742120118aeb73095741a1bc8e7e2b6eab0428b40a", size = 3098945, upload-time = "2026-10-20T00:18:45.554Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f3/2555a350f724d19d78177e9b30cad008a0a1b834d92e0ade17abfb4159c1/pymssql-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84384770e186b8a815083d49b95881ccb594bfd1637f9977d209355f65546569", size = 3276985, upload-time = "2026-10-20T00:18:47Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fc/3218bf5aec92695b96b1a4d7257ec5b3587621db4b958f305b7e57755356/pymssql-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d7b7aa9a1344109657839ede7b7ba7f32d439ff3a4dbaf92dbf9b7512fa2054", size = 4490170, upload-time = "2026-10-20T00:18:48.381Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/afd87e617eda6664411452a6116fe910ea0275e396af9790099b689f08ad/pymssql-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc169c52e55e94321696a79f9a664480d908b917364f04d4a9f7f7f85892146f", size = 4225176, upload-time = "2026-10-20T00:18:50.94Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b8/32c81774d55633726bd5769370b30e717921e34f82bf583bfc8cec821e37/pymssql-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:71d8207eb4d4fc887ceabd0bcd325121b8563d76bdf0710b80f0ed9ea44aef06", size = 2377940, upload-time = "2026-10-20T00:18:52.273Z" }, - { url = "https://files.pythonhosted.org/packages/df/88/08f429c4221c8723adc0d9945f34c1eecd1ec9889e6750c9aa1ace608b61/pymssql-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d802451b025fca7856faab16591de54e59fbeb4d9d8ffd1f940c18c916fac9b8", size = 1908759, upload-time = "2026-10-20T00:18:53.583Z" }, - { url = "https://files.pythonhosted.org/packages/db/19/abb08f7d5539d2f7211cf8f98091d430e4b0eebb7029bf1bb81572034afa/pymssql-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:4095d1b6a230940dd34b1c2db870c1b1051d893e7c7a62c832e23bf2d18b5ceb", size = 3227283, upload-time = "2026-10-20T00:18:54.897Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d7/bc8d98491c42d13dae0b78bf72d0a62d1a563b0c363524fb4b72c1489386/pymssql-2.4.5-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:7148cacb61e4faffd115a946d2e9ee2e763b821c49277350fcc44703bc78549d", size = 3020500, upload-time = "2026-10-20T00:18:56.571Z" }, - { url = "https://files.pythonhosted.org/packages/f4/53/31647de8e1f7ba10ec60d7ada60aa09e8e224eb9f0b2e1e1a40a27b92d90/pymssql-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d0ff45ad3b5dc204d0fca928e654fc99ce50ef5ac65c33ae98ceb3ee8173a3", size = 3093132, upload-time = "2026-10-20T00:18:58.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/90a4d8c785f1b055f306db601076022fde2389bd9170642a8a08bb53770b/pymssql-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca142acd30fdf84bb4e19adf4fe732e31dd7f34557fcc8fdc11d001cf42e3d60", size = 3273179, upload-time = "2026-10-20T00:18:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2f/52b93a66a51558c0b523b3e1c67eae08e7e91cde8048b4c7fd05c6744318/pymssql-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0163a6f89fc53b2dbd2a11c2c500f52266a38257cac0a8e8dc7f562a4c9221", size = 4485244, upload-time = "2026-10-20T00:19:01.104Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3c/f557765e794846c1d5065b6e2e3fe96df08df2b02aee03cb1f89f1437bd4/pymssql-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04050fa1d5f68ff37ead859f877bc4f217240d20f8f770d5b694ac09cf664c3a", size = 4223450, upload-time = "2026-10-20T00:19:02.547Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cc/9c7640d39a5aed4bd6cd83a594ad8b282174ab789edfd9253574c03e1f78/pymssql-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:2f8441656e016a0f3cf0e4181efc054f41056b11ffd54cb774ee0e8dbdbf5a2f", size = 2396761, upload-time = "2026-10-20T00:19:03.954Z" }, - { url = "https://files.pythonhosted.org/packages/d5/50/cc975d4aa3adcbba3a0610b59fe86f01b0094025f4c91deb6d263a5fbf8a/pymssql-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:1e5369709fbb82fa710a5e1f7ea67a4acfa17ba61f86c7be6384d3cbd9a35350", size = 1923588, upload-time = "2026-10-20T00:19:05.407Z" }, - { url = "https://files.pythonhosted.org/packages/01/3a/2c31ea8f700254d86cd040d5373001c82339f798c589cc58a1415a3fea38/pymssql-2.4.5-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f723cfada20b6679e48cc8fc8ad5b9caaf3467168d622ba83e3ec8dbee239431", size = 3198417, upload-time = "2026-10-20T00:19:06.682Z" }, - { url = "https://files.pythonhosted.org/packages/33/f5/28e66eae4fe417b976b894d8fdc4afca72cc8ae19da3e638433c14682e6f/pymssql-2.4.5-cp315-cp315-macosx_15_0_x86_64.whl", hash = "sha256:dab1cde0a1ccba53eea96d0156528012d06e442c70bafe9605828ec41ed9bc66", size = 3004519, upload-time = "2026-10-20T00:19:08.048Z" }, - { url = "https://files.pythonhosted.org/packages/ec/54/49b39f6024924db86388d0da46c3335d24cfef88acac971b61a462b01494/pymssql-2.4.5-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:372746a18767b017715fee03da4151978087841edf04e9b45d7823e7dae5e8a9", size = 3099426, upload-time = "2026-10-20T00:19:09.581Z" }, - { url = "https://files.pythonhosted.org/packages/49/17/413861f098dca28193fb1a9b0f42777e7d94dd34384968d02896000e06a0/pymssql-2.4.5-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa7463e48274d1b5cdaf882a6e9ba3ae06807d451819351373398a2f56e218f7", size = 3277041, upload-time = "2026-10-20T00:19:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/2f/14/9a35456242fafabb07c33dab7a39cb8d69ff73296bb3c7947abed717f11f/pymssql-2.4.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:09dc94f8a13c2b332e5aab768682ea64ef05c50434498274812d8a94b0a80c1f", size = 4490455, upload-time = "2026-10-20T00:19:12.429Z" }, - { url = "https://files.pythonhosted.org/packages/e6/56/bf6888c70270862302f4d186e83995261e1072fecd60dd1c84bdc25b5cd4/pymssql-2.4.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:5818445136cc3180f0e92f3ab081226344694e247de4ae57b38ba0126a6d760c", size = 4225373, upload-time = "2026-10-20T00:19:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f5/f859311836c84c79138646fb0ebaac359644318eead67db03505c6e736c3/pymssql-2.4.5-cp315-cp315-win_amd64.whl", hash = "sha256:357a7925d2a1f3aeede56225c177b1aa9685f346afae0bdab0a2b794951f3245", size = 2377625, upload-time = "2026-10-20T00:19:15.223Z" }, - { url = "https://files.pythonhosted.org/packages/c0/13/f7bde703726eb7168fd53f1308e4e179dd18d1fefb5393862880345b0eae/pymssql-2.4.5-cp315-cp315-win_arm64.whl", hash = "sha256:93e68273e3e2807740dfe6a99bc4bf3876d5417b6ca2dd95632839d0c73f00b4", size = 1908218, upload-time = "2026-10-20T00:19:16.89Z" }, - { url = "https://files.pythonhosted.org/packages/73/67/dcd7e6395cccc9d3a148667248b589dc256a583058d81febcebeedf2f9b8/pymssql-2.4.5-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:a4d5e230468bc882388ec9a37e185a43de45d5182940c2b85e7ca25bad516828", size = 3226657, upload-time = "2026-10-20T00:19:18.129Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f4/0835bb557210f16ee5eb01bda713891f077c1f730d0a241114c54d09c79d/pymssql-2.4.5-cp315-cp315t-macosx_15_0_x86_64.whl", hash = "sha256:49ed1b078760c41f7ff632174c0531e825dcfca2b2d541236c5547a9266ada63", size = 3019612, upload-time = "2026-10-20T00:19:19.454Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/a60984fe167f2a96855095cb51c1764398c7b60f822e7c1251d4fb837be8/pymssql-2.4.5-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa63dbddddc73a2487cabea1c7e18a32bd7043f4ae5af2ade414b580c8427c2b", size = 3092609, upload-time = "2026-10-20T00:19:20.997Z" }, - { url = "https://files.pythonhosted.org/packages/1b/42/69c782c0512d90421454ffc22a6c7a9b3633ede160977c7313cf56395e70/pymssql-2.4.5-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea3f28dcc8e54beee478c0d4c2d92227ac279a46f60f4f1dafe7abf84b82141", size = 3272590, upload-time = "2026-10-20T00:19:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/01/b4/f7fc61acb4e8b9b3cb334ea5fdf9ed614282de2f7fbc3c502bb5171afaa6/pymssql-2.4.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c91ac0938f2bf6702e36e00791b40535c6ef13f870407472ce1b0583666b3c0", size = 4484295, upload-time = "2026-10-20T00:19:23.81Z" }, - { url = "https://files.pythonhosted.org/packages/18/63/d22117e3d5a00b3190e324ea71655af9948041f6c20f3d412bfa84bc0994/pymssql-2.4.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c30fe08a7ae5f412758bff2f256bcb92bde9bb2d06f44eabee9b7780fb5119ff", size = 4222807, upload-time = "2026-10-20T00:19:25.496Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/475e602ea569836312d2e8aa9934e9a9123b47b987a4aa2893ff6e09038c/pymssql-2.4.5-cp315-cp315t-win_amd64.whl", hash = "sha256:d0b56fe1dc3925487d47a74dcf02a28ed0b58ae19ee9dc2a0b1c5e93f95266c5", size = 2394303, upload-time = "2026-10-20T00:19:27.756Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/b2b2cfa13e80efbeb7789fdedb55941df51c56ff46a85add7bfa9df80d2b/pymssql-2.4.5-cp315-cp315t-win_arm64.whl", hash = "sha256:9e7de3a928d495b629f8f0b341c90746ba5e8e8c7b8d45d46e81a8e2c6e3db0a", size = 1921599, upload-time = "2026-10-20T00:19:29.296Z" }, +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/3f/5d66aa70242731bfc6b6c62b14e4c6e9db0cb2dca778f0d24411b32bf88a/pymssql-2.4.4.tar.gz", hash = "sha256:6cd20778ca0e01bb5783c6d65fd2d2f2f5ca520015a4f86930d2a58ac72a0e3e", size = 211055, upload-time = "2026-10-10T04:09:49.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/72/b7f6f1caf55a4b0d96fc13040a66c05b36b84e8f7aeb8e1de649b57d7064/pymssql-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:db24af7e4185d4219b424617c7d8f831423ed8b48929d1f1d43cccd907a87b75", size = 3210125, upload-time = "2026-10-10T04:08:18.794Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ce/5c290dd5eef7ad579e1a4b0dbf622fabbe2485505d96f69087dca24c0286/pymssql-2.4.4-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:d6ccd0e323ece21a14c1364065fe9bc3176dbd03870557bd974ad37e80961830", size = 3014314, upload-time = "2026-10-10T04:08:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/37/34/5f99945ed455d829ce93fda685ddc8a54b2941664d588342db323689c5c1/pymssql-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98936558f5a588fd26b89e020f80b8e7c7f8d7df3a8e239405dcedf5de88dea", size = 3067447, upload-time = "2026-10-10T04:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/f193fd02c19f98b539180b7cddf460f960c93bda227e711e26f887e692a6/pymssql-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8035874c7d12b83c54931cc014c761229fe480c30fc0793c0b7966496c80f530", size = 3246650, upload-time = "2026-10-10T04:08:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/f3710033a34ee7fb56558f9454ad8b39d259496d3026dd19e9065a57ad74/pymssql-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfa2504e280e54961508de51c7e7122ef309c74d609118e365821a444220df61", size = 4459721, upload-time = "2026-10-10T04:08:25.377Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/905dbfc6829e0521872016749c5f1998ad19db667f29d59477f0cf366577/pymssql-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e459565167cffb0f2a82aed3175cd7e50e6697457bf863f7b82dd00cc4993032", size = 4196191, upload-time = "2026-10-10T04:08:27.107Z" }, + { url = "https://files.pythonhosted.org/packages/61/04/d55524256a8a78631cff38f003494d04677a6dd736569403359ba4a8d023/pymssql-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6ae241b1f57a543111cde534f3e379c2df1ea3064ecb16e1fdd4200388dd9948", size = 2324698, upload-time = "2026-10-10T04:08:28.87Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f9/e5af55e9c2a673a746f283fb34539f78ee5e4f5b2b06de874811f781dd34/pymssql-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:860446859caba331dd3b2580f2f384f64f3c536a6b20c1b90261078b22868ab0", size = 3192145, upload-time = "2026-10-10T04:08:30.516Z" }, + { url = "https://files.pythonhosted.org/packages/14/c1/088c959221eaf4ac12a05f02f034b87b5cc4f190ca42cc3d54993f14b177/pymssql-2.4.4-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:50d74b1171da25b0d9bb09f0186995e28bda02a65f95ccf4a3fcf28b204943a1", size = 2995170, upload-time = "2026-10-10T04:08:32.279Z" }, + { url = "https://files.pythonhosted.org/packages/84/f5/b15e5ccdd6564a73820b536511b7a3862c25fba1a24744655a18b9b1104d/pymssql-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0090e3684cc6a1198c84cb59945f6182806c2fdaa24442e6a0b06154d5b3a31d", size = 3097963, upload-time = "2026-10-10T04:08:33.905Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/3e6e3fd8a8e8a3a68a1c21afc6447dc5551f30fa496c726b07952d893374/pymssql-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f887aed9155f0ba8ba49ef06cdf259a7c0ebce329c978930dcdcbbc901ca4d98", size = 3275346, upload-time = "2026-10-10T04:08:35.342Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/9d20fb679200ddbabf57e001019646080832bf2251c32d75460439860353/pymssql-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e8d5e01fd4ed17274e0005082ce47fae1993681a1b2b05beceb9ef77d130579d", size = 4488379, upload-time = "2026-10-10T04:08:36.821Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2b/306a16194937d5ca36d2dfb3d13ef06c7085f2ecc0198ba83e101bf9da0f/pymssql-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd67b469836dc69979b13d977724377942611e8b8e4aa6896ea7a53139a15a99", size = 4223503, upload-time = "2026-10-10T04:08:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/60/0f/2146384af096f9ff0c23f30f7b9db57b741cd52d1db6940835e52b2f9584/pymssql-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:af196a9f57e06745e4dc956dae801faab853240836b9047d9068c1d4020b6b69", size = 2313364, upload-time = "2026-10-10T04:08:39.979Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3e/a133fad9e5d0e40a5d3d8826ff073874f6335e89d234a70b110464f139c7/pymssql-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f255f76f543d90eb0b471e252b0b0e2858033138f20f5ce261a9195105e6ed7e", size = 3192233, upload-time = "2026-10-10T04:08:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ae/1fa071f438ed7f414861afd3732ecfa1268b3e6a0317053ba1bc76e3ff61/pymssql-2.4.4-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:4fdaf0c55c49be5488ba515788b2d36a27763b0b8b345aa483b9fd3bd9a95563", size = 3000180, upload-time = "2026-10-10T04:08:43.322Z" }, + { url = "https://files.pythonhosted.org/packages/3a/92/c9626cba839ae336a8a120a626d4cc533d53f52ab10b68734bdd55da7fa5/pymssql-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19a5af6797232e9c4eb396f86bc3779ff237b224e2eadc81486699762f36d62e", size = 3097494, upload-time = "2026-10-10T04:08:44.875Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/6070333af8ce801b884e7c9a0050b4d5278f2f563cfa405f68a94e711bf1/pymssql-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12150aae35d961986d1ffa6550cac70349798ffefea9d4403a9a9943e1b6d394", size = 3274897, upload-time = "2026-10-10T04:08:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/c71d3291e03b3e07dfe8fb104dd3a4520fc61fc2ad5f7b3385b0e0ca4be6/pymssql-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e6c0d774d3aad54df6f7c428dad346f718704eeca9ccc270f8e4499ecc2e9f39", size = 4487921, upload-time = "2026-10-10T04:08:48.389Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1b/18c887fccef26fd89ec2c233fe5e6606047e6e5ade9480897208225011f6/pymssql-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:59e47602c4c9c5c221cba8bc9e44f1cbe0343ec23dbce63243065a4b7979e4dc", size = 4223072, upload-time = "2026-10-10T04:08:50.212Z" }, + { url = "https://files.pythonhosted.org/packages/f6/88/5b7b335153a0f151852e8633b672d7e6ae54c38dc1e7d13cafa929129d02/pymssql-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:405823931c3dc51ad5442f8a04a0e17cddefa0c215684605d9e5c137b6f758a9", size = 2312567, upload-time = "2026-10-10T04:08:51.642Z" }, + { url = "https://files.pythonhosted.org/packages/d2/56/cc79d0f1c41c4ec5f70213bb1be60da1deab85c40ceaf36d4b9a4f8c271d/pymssql-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a8a35ffddbe3d5320fd5b113e9d3cf6f4876be2ca155c3618a048850c6d420b4", size = 3195642, upload-time = "2026-10-10T04:08:53.097Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/f56629a6c873d2d7a9737502c2544c4df43ced7a18982f71b11895bb3851/pymssql-2.4.4-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:c070f5b97466daaae2dbd55643fe3de9951bb736e80aaaf82a06e0e6d55a19fc", size = 3002116, upload-time = "2026-10-10T04:08:54.534Z" }, + { url = "https://files.pythonhosted.org/packages/c1/36/185dca7f4bb691d77ff853258bd7c7d55cd44fb8f44a5b79ab4df09883ee/pymssql-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b7a5766d4b2898e791646196af4ad8618a601029396dd13bf1317280c525599", size = 3095511, upload-time = "2026-10-10T04:08:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1a/ea7c498f7736209d27eb99f63eaad6121d21b25b19ebfa5f559d27d37642/pymssql-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc0fa1e68694d15ac8fee064cf6674053eacd51892fb8538cf3a703cac0a561c", size = 3273070, upload-time = "2026-10-10T04:08:57.345Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9b/18d141d421bf28cd3ea39c24cac45610f26c448efbfcc1cb4723a4e4ceba/pymssql-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3f039045feec1b0c60c9266fb27e3d39b4bcf8e69eee7485d7c4ead7adfc56b3", size = 4487159, upload-time = "2026-10-10T04:08:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/42/9b/87106c146316036a5cc19b2b2bf8d33d3b207f45f30f9525b6650e2ab05e/pymssql-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f5731261eec9761ea76fbb517834c2ee5e3ae6f689e8bc517923264a4886a184", size = 4221646, upload-time = "2026-10-10T04:09:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/f954af0ffd773731135c87add639ef01248b7646ad670f4db08db4c444f9/pymssql-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e86cb1d1ae4c6fc27fd0c0c3c2409d15c3bb9ba2dede52c4633eb8209f2081c4", size = 2375068, upload-time = "2026-10-10T04:09:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/523a6b9f7e6b26b86cf0ce9996f51c94082fc480a028ff9ed44e86b900f8/pymssql-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7e6b0105dc5c3648095c528ab08704f49f0cdd3f3f6cbca8a3c317f11aeab281", size = 3223425, upload-time = "2026-10-10T04:09:03.782Z" }, + { url = "https://files.pythonhosted.org/packages/ac/95/e45755d00a99299030ac2c17f09bb0cbad492b41c58f006a634246b90109/pymssql-2.4.4-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:8dc95ccb06863883015e8340cfb2c7e1ca889ab789c7109326de3231412149a4", size = 3015996, upload-time = "2026-10-10T04:09:05.584Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ae/36e9ee73e1550bde1ebd106bca02df266758e939f9a9d3b1ec5932b338cb/pymssql-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ef6906a637106141f316f3215b9772104da52409b3b1b860baecf086ef9365d", size = 3089543, upload-time = "2026-10-10T04:09:07.9Z" }, + { url = "https://files.pythonhosted.org/packages/95/39/62da8c248f65b5914174d6380f4f61ba398a82303c0de24ae11c79a7da1c/pymssql-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae93ad536e7b0b912e0356354ffef7d0f2688251f37d28ec31906c59dae7e088", size = 3269139, upload-time = "2026-10-10T04:09:09.278Z" }, + { url = "https://files.pythonhosted.org/packages/ea/02/3c2ebfe8ac54ef2eeac7e31b91133731497abed7ab2ef21276be04d5358e/pymssql-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:484f02d2ed27c47d90d72a2dde50afe6d1274e6698f38538dffbd53bc4701ca6", size = 4481795, upload-time = "2026-10-10T04:09:10.89Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/cd6e9364baa39fb4826e1bf563671fa12d50f39455bf14d3dc220c258c17/pymssql-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f9e423685beb39f8568c6bb5ef7b6945bc10289e0369e66e9498a85255924aa", size = 4219628, upload-time = "2026-10-10T04:09:12.495Z" }, + { url = "https://files.pythonhosted.org/packages/76/43/e20c9cbc72aefa7f5111daa632a38f24ee05ab7c09b9d8f27171e87ae1a2/pymssql-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4404ff0d9e7b0593ad7039b037a01d6a34d7b9797017187f197384ac24b981e0", size = 2393053, upload-time = "2026-10-10T04:09:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/13/98/baefb5f3fc477652b57306d71abbaffaf0ca2974d6b95d16517b6987b622/pymssql-2.4.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:4d46d723be19f1a103979d2d42ab720b218be8c536a757cbe017a4a82141d274", size = 3194650, upload-time = "2026-10-10T04:09:15.74Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/7e5ae04bc955af9b19d5ee46890763ba8afc466027ee80c8e4ee67a576b8/pymssql-2.4.4-cp315-cp315-macosx_15_0_x86_64.whl", hash = "sha256:72b3f34b5239dab8d87e47ad025cde11fea80fc48dbd3951a9bc5fcbe03b1ec9", size = 3000838, upload-time = "2026-10-10T04:09:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/99/4c/203448fc30c13537bda345113bf3dae9e0546ee6c60d89e6ec2d1828a571/pymssql-2.4.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0155c4bd8751eb6ca89c07dd0cd6a2b1a7d063efb6048a01489527239e10c5f5", size = 3095778, upload-time = "2026-10-10T04:09:18.965Z" }, + { url = "https://files.pythonhosted.org/packages/69/a6/c36c1c33c2a97131438eebed6cb306627be49e7f5af0b2c6c83732f6ed08/pymssql-2.4.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3dbc9f285a938f8642c8a138bcbb4a2ff002c5412521cc4093a149b21366d611", size = 3273194, upload-time = "2026-10-10T04:09:20.558Z" }, + { url = "https://files.pythonhosted.org/packages/ee/db/55bf702c56498baf6ea7251dc662927c0dc68c690389cf7b5fc335cad3ce/pymssql-2.4.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:604681336a1aeb716f3decbee51edb7de0d9c81402f18560e4d575787df61921", size = 4487327, upload-time = "2026-10-10T04:09:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/54/e4/9bab285d4dbf7f062ea28e5e2d9b25aba3d560601c48e65bf2d99c110872/pymssql-2.4.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:89b69fd2f77f51552760d42941d04bfff79ff51211a6313de6a25c8d5aa31685", size = 4221768, upload-time = "2026-10-10T04:09:23.92Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/13e0202acdc769fc8a8baddf564c4cf1ee0019ef31dc4455afbb8c614683/pymssql-2.4.4-cp315-cp315-win_amd64.whl", hash = "sha256:b44e4f781eac5119061eb4478e6025dc05da98db029df185522eb3c38a5cdf2f", size = 2374420, upload-time = "2026-10-10T04:09:25.333Z" }, + { url = "https://files.pythonhosted.org/packages/2f/19/bedbcbb98ac6624fd21f8dd59ef9f69de2a07402cb8132a28d362bdb78f4/pymssql-2.4.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:db8c0d04afa884b57e19d45b025b10767ee7aaa5e5ed9758e536062319822cb9", size = 3222833, upload-time = "2026-10-10T04:09:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/1183858e50511d1088029ef1c0a2fe59efce40abe3d5e69f7eb0670dfe73/pymssql-2.4.4-cp315-cp315t-macosx_15_0_x86_64.whl", hash = "sha256:8541a520f0355e31d7b1ca30829dc383ce1ab4530dc6513d8fad97e791aba07f", size = 3015114, upload-time = "2026-10-10T04:09:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/49/22/22b070d1d254b62ddfece4c789a713d7dfddf3885275c8bda38dc50d66ad/pymssql-2.4.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6606a68ba05f467d7d4ed6e979a962eca84fe57adb74957acb1ed62953387d1f", size = 3088943, upload-time = "2026-10-10T04:09:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/42a5fa47a8cf077b7755cb873dc8c52accc83901b4828dfa3838032ccb42/pymssql-2.4.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eafb731d8fdec7800fa738c4b88062eb712c742caa87acc953654e5a9ab7484f", size = 3268608, upload-time = "2026-10-10T04:09:32.072Z" }, + { url = "https://files.pythonhosted.org/packages/cc/39/f7c339842ad568cf7a12ba4ff3d317f402c1d31d9896a6e70f27a163fca7/pymssql-2.4.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6651063f5ec2ab7be80ccaff7442de17e32ad6d7cc9b6cbda66b9b0ba8b3bc90", size = 4481171, upload-time = "2026-10-10T04:09:33.749Z" }, + { url = "https://files.pythonhosted.org/packages/32/f3/ec46b9c5721d3292621c1b8f49b61a45da7fc032cffa5e97d022bcaef486/pymssql-2.4.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51bb4f577da737a06532d8e3292d578a5aaec942c1461e05735c2b57471c91c5", size = 4218909, upload-time = "2026-10-10T04:09:35.289Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/d566b6101d4a22bb953e61489f6e9476c2fafa828ae4d9c2afc9ecf7e9d9/pymssql-2.4.4-cp315-cp315t-win_amd64.whl", hash = "sha256:54bccdd09ff7714b691cb06c9b9d121424ad0338e20ab9ca027814765033174a", size = 2390318, upload-time = "2026-10-10T04:09:36.749Z" }, ] [[package]] @@ -4869,7 +4880,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -4881,30 +4892,12 @@ wheels = [ [[package]] name = "pytest-databases" version = "0.19.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/litestar-org/pytest-databases.git?branch=test%2Fclientless-all#735e1527bf38b9208495931c908d66f01ccb8e98" } dependencies = [ { name = "docker" }, { name = "filelock" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/4e/e899556b3876eec2db9bd630ad3054ced94a9541c26319bec5c4cd00579d/pytest_databases-0.19.0.tar.gz", hash = "sha256:ba7b8e51b551455daf3bd144384f6d4fba23d747b001f071795b02e6be2a3cbf", size = 350034, upload-time = "2026-05-23T18:33:49.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f4/d6e2edae47a0421a8531a9805fb787b993380222f00dc25cee1634f55f3c/pytest_databases-0.19.0-py3-none-any.whl", hash = "sha256:d14651e23a716ed6f2317bf9ac317c9e7891db701253abf78e9cef1049e8f26b", size = 41525, upload-time = "2026-05-23T18:33:47.983Z" }, -] - -[package.optional-dependencies] -bigquery = [ - { name = "google-cloud-bigquery" }, -] -oracle = [ - { name = "oracledb" }, -] -postgres = [ - { name = "psycopg" }, -] -spanner = [ - { name = "google-cloud-spanner" }, -] [[package]] name = "pytest-mock" @@ -5072,18 +5065,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "questionary" version = "2.1.1" @@ -5152,6 +5133,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/51/b2b91aec091e86bc961e5164f00207d89f133cc82b5ddad541d2132ea819/rich_click-1.9.9-py3-none-any.whl", hash = "sha256:365e7a9d0adb42e41ea832a0a12e02c44c079a26536dee688125eb9814f97274", size = 71910, upload-time = "2026-09-05T19:20:13.587Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "rpds-py" version = "2026.9.1" @@ -5289,27 +5279,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/58/690cf56da80c8f2c8bf12042b6017beac2c34fe961f65420bcb3fb7fac38/ruff-0.17.2.tar.gz", hash = "sha256:84d632402d14c23b488f941db23712d6736a013225162062d34558c3f3c780eb", size = 5333400, upload-time = "2026-10-22T18:38:00.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/b8/8b6d69c57e8fe1cc6af1aa7493b72a1a5b5a8afe34694e302025441729d4/ruff-0.17.2-py3-none-linux_armv6l.whl", hash = "sha256:96a65e755d8dba094707fb23ab602111d9a31ec2835b3a6992a1914a8fcb211e", size = 10183857, upload-time = "2026-10-22T18:37:14.956Z" }, - { url = "https://files.pythonhosted.org/packages/e8/61/12cc2ff7b2fa90a268ce1e2162c23aaf76e601d22623f6a31b0eeaa764b0/ruff-0.17.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:39c5b55c960515cdb32be16e69efb5734c589c06a0eb38a22694524a336b889d", size = 10210904, upload-time = "2026-10-22T18:37:17.985Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7c/b8850a8e4ad29f64a827dd6c6902db4a9aa538fb2b9eef4e26084001a3bb/ruff-0.17.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:92a80d2f44584dabb0189b3a6c393e37e525e04942838482010575cb81945be7", size = 9408439, upload-time = "2026-10-22T18:37:20.964Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b6/6a64fae86ca5e5e29d5adc311e5481de726cedf9aee607435a16c31c442a/ruff-0.17.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7d0906677d89bbf3cf73cde64b5bbe2ab35df3ae43e49ee2627301e91265bac", size = 10047078, upload-time = "2026-10-22T18:37:23.717Z" }, - { url = "https://files.pythonhosted.org/packages/10/19/23d376547715175aa1bd46df48df6aacc7c6439d9daf19cac15975180aa5/ruff-0.17.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cfd783590edf2b77317882631900f6a13ea2e8eacefe6ebf2370d2715299160", size = 9990478, upload-time = "2026-10-22T18:37:26.584Z" }, - { url = "https://files.pythonhosted.org/packages/93/6c/34cda32cbe13adac5c6b291a307a5570d8ebd37c0c86339f0fe628e55632/ruff-0.17.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122258f6ecde0e0dd1bb60b24263c3edde4338caf723fca711cc729f9bfd414a", size = 10673624, upload-time = "2026-10-22T18:37:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/bf/10/5d762a4dbc851d8bb47863cf3a28a61bf31c804d393b70d891fac899f5d1/ruff-0.17.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8fba429d6ac4128ab29cfb8fa6240d2fa671d04adf7f84d3b22fc7b6b6bea4", size = 11351797, upload-time = "2026-10-22T18:37:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/3f/04/a0e92a81faf5c81c1c98d4374c8401734ccf2b8cca7d0170cb0fec08c6e2/ruff-0.17.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bd67bb9e7061dd09bc46a0a884bae2bbd214dbcd48e509813612619683869", size = 10949414, upload-time = "2026-10-22T18:37:35.501Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d8/59894cf630d2d86882457a2a6a0a1533410c5c805bfbac6f6eb4b5aef69d/ruff-0.17.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60f93fec7142dba5b8f487f94bf2f6231dae24dc11e4c48648347c8b0720004", size = 10528000, upload-time = "2026-10-22T18:37:37.88Z" }, - { url = "https://files.pythonhosted.org/packages/ab/95/d6e30c51e851d37e10762ad91fa1e76ddaad998589b5667962a3cfbc9edd/ruff-0.17.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c555a877f8f41447e318101cfefea9bf12dfa9f82eedf90c376e57e069f917d0", size = 10571779, upload-time = "2026-10-22T18:37:40.444Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5a/a7e1d74fb464250137d3d4eca645d323536533b672004d04ee0e46e0caf3/ruff-0.17.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ba405cbd5ec7bda1f555d5e52be1fd68d02608d4d95d24591bdac92cfacb579f", size = 10051707, upload-time = "2026-10-22T18:37:42.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f2/02fca29ca7999380f0350a7212fe755c11ab9c22d61327356cce8decdbc6/ruff-0.17.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c21b4ab24485358f17291a4f8dc5399b834e963b8efedd920fe4fdb02081bb4", size = 9998227, upload-time = "2026-10-22T18:37:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/00/55aa604a093dfdc1526e570fae0bb7d37a2221c7b4728d5f080b2add16ba/ruff-0.17.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5875a16b0de70b43cb81eabea5ae5e8f5d1b2fd1b5e6ed07f80cfe5019d0bd5a", size = 10377692, upload-time = "2026-10-22T18:37:47.704Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5a/a7a640f47bb906ec8fa233ea84319d503e6dee02b59e3901bd312ebd1f9b/ruff-0.17.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d086be452eed686aa51271ecf2cbe5037956517cc6ee39a17b4489ac12f8bbe", size = 10790587, upload-time = "2026-10-22T18:37:50.191Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d4/c3b74e1b4808438ea7f14bb914478025e2d51261f60774a54a0fa222ec46/ruff-0.17.2-py3-none-win32.whl", hash = "sha256:6bce7d1f5165701ae18f5df85c65afe78cddfa6c5fb4b9bdb0014d60388f3edd", size = 9870537, upload-time = "2026-10-22T18:37:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/c7da96c2c96d20704cf9027f7a5121da272a8c05f1d1531ba949e3b3df04/ruff-0.17.2-py3-none-win_amd64.whl", hash = "sha256:1b59d447f197bef9ef48a22ba1a6016137753c6099e9bac9c36eb566e422424c", size = 10282826, upload-time = "2026-10-22T18:37:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/81/60/a3604c79cb02777b6cb84da894104021986f06c4bc41375102cc95049a32/ruff-0.17.2-py3-none-win_arm64.whl", hash = "sha256:195ad918027f817c119310ca89a42d347887dfc28934aaa869325e9c5dc9d962", size = 9827036, upload-time = "2026-10-22T18:37:58.031Z" }, +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, ] [[package]] @@ -5371,11 +5361,24 @@ wheels = [ [[package]] name = "setuptools" -version = "84.2.1" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "shibuya" +version = "2026.7.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/b3/c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1/setuptools-84.2.1.tar.gz", hash = "sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc", size = 1230289, upload-time = "2026-10-20T19:01:37.742Z" } +dependencies = [ + { name = "pygments-styles" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/d2/8017988e02791162b286576116fedd200ed33e8d8af3579ab3587df3d3d0/shibuya-2026.7.12.tar.gz", hash = "sha256:9496606f5b95595511ad86b773d74b4b8d694a4dcc2f848e6ec34e2ad71d9060", size = 86545, upload-time = "2026-07-11T15:41:16.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", hash = "sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", size = 876263, upload-time = "2026-10-20T19:01:35.678Z" }, + { url = "https://files.pythonhosted.org/packages/84/70/9ca2b8187666be1bcfa82b938107247159f5b566a22b5e43be27df45039d/shibuya-2026.7.12-py3-none-any.whl", hash = "sha256:0a2c0c109e7cc47fc650b01feb671d267c0ffe2e39c98f3529e32d723a9c9d9b", size = 104092, upload-time = "2026-07-11T15:41:14.936Z" }, ] [[package]] @@ -5405,86 +5408,292 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/6a/c0360b115c81d449b3b73bf74b64ca773464d5c7b1b77bda87c5e874853b/sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca", size = 20869, upload-time = "2026-01-02T15:23:45.194Z" }, +] + +[[package]] +name = "sphinx-click" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/ed/a9767cd1b8b7fbdf260a89d5c8c86e20e3536b9878579e5ab7965a291e55/sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c", size = 27035, upload-time = "2025-12-04T19:33:05.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/bd/cb244695f67f77b0a36200ce1670fc42a6fe2770847e870daab99cc2b177/sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7", size = 8939, upload-time = "2025-12-04T19:33:04.037Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + +[[package]] +name = "sphinx-iconify" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/4e/498964c2a6025e489b255015d3b5644aa93eb1dd1035324de18179af8abc/sphinx_iconify-0.3.0.tar.gz", hash = "sha256:7824ca694472d6babbe811e72cdcaf52b6e0ff4240e8cfda721f84f867605611", size = 3701, upload-time = "2025-12-18T05:19:03.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/6f/7dd3097d8ce08109cff3e63a771ca1ad795d8d63d8015eaa6fbd13f784fa/sphinx_iconify-0.3.0-py3-none-any.whl", hash = "sha256:b38ca4a66767c317bea31850a6f540ef79db4394eb4792961918c2d44b4a2c3a", size = 4068, upload-time = "2025-12-18T05:19:01.892Z" }, +] + +[[package]] +name = "sphinx-paramlinks" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363, upload-time = "2023-08-11T16:09:28.604Z" } + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/87/06537dff20aa57df2d0f2afa87edff4afa85dec6f57f03f7acc24f83ff88/sphinxcontrib_mermaid-2.1.1.tar.gz", hash = "sha256:f8d7f1b2f0ad69b7ebdd5cbf532fb4ee107893b5373bc2d0e9634089c0b2cfee", size = 22353, upload-time = "2026-09-01T16:10:23.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/6a/5bf2222b8084c7a7bc3ec23d470c91e3e03e82e4ce0bbacaa61f456ce7c1/sphinxcontrib_mermaid-2.1.1-py3-none-any.whl", hash = "sha256:3b9ac1a8e0b5154dec3c0acf9b5c54dd9ba58540e416f20858c300814c25648f", size = 16368, upload-time = "2026-09-01T16:10:22.377Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "sqlalchemy" -version = "2.1.6" +version = "2.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/dc/9addbf490110b5727380059ee542ad8353bdcee45a37ba6a5e0976966ae0/sqlalchemy-2.1.6.tar.gz", hash = "sha256:499eae5c9051efe96cb9a73ec2fbde8ffa4186f102b9094ebab8fcc9d3b99f52", size = 10589700, upload-time = "2026-10-21T16:25:06.497Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/e1/fca57b09555d90ed6f8ca043245d5205cb2269122acdf8eeee32176711d2/sqlalchemy-2.1.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4e4a43cde0dcdff974304ddb48246d6a094e618b6ee9642827ddbfe9fbebadb", size = 2471480, upload-time = "2026-10-21T18:34:54.083Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3b/e593c3cbb51713e6e646409a5de893c1b6432e45339c7741a6a2edfbd09b/sqlalchemy-2.1.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b6b5fdcdb6891025117f76f48d7d0f89245d66fab6519f3b856294bab317387", size = 4636734, upload-time = "2026-10-21T18:51:15.747Z" }, - { url = "https://files.pythonhosted.org/packages/4b/69/8a1e812d3937f8367b64cf0ddcd2559cf9b0619c4061ebf08ff8084b949b/sqlalchemy-2.1.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d877b2c46298825e8f32059e8d912d0d100af0e9fefe31fb2abb52192817afb", size = 4658995, upload-time = "2026-10-21T19:13:58.837Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d4/7d42a5f4efdb859fa36e85168fcc3c824197193439ff7fac2b595bc94e14/sqlalchemy-2.1.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed6596fb63c574fd3dc4d9ea58a7b9c6e7235dc3d74a0bd3791559d5a618e947", size = 4389012, upload-time = "2026-10-21T19:10:55.617Z" }, - { url = "https://files.pythonhosted.org/packages/4e/71/6395a7d168ce7f25d2c3f9b6f51d1ca775482c8510d96bfe75fe880ab91a/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b16a8960ad9cdc923b90dbf431a4739fc81a07b53374ff74b7bdad3be31ce189", size = 4574669, upload-time = "2026-10-21T18:51:17.529Z" }, - { url = "https://files.pythonhosted.org/packages/54/b4/d9b221dd36f82d0e34413837deee25fef45cc13dc4e0483c7ef26b1c34f7/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9245bcbcc3056d440f82e43fb34f94c15361c86b62e14cd9d7880c8e0cf370f6", size = 4385919, upload-time = "2026-10-21T19:10:57.39Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b6/9a8a6f4c2e4035c0c3edc689de4e672ee1324a1fca22f7d9b775b3b87f33/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a909a8eb82ce5a2f1566ebd685a79f3cfd9e049d57e3a0d3e24d56da04913e73", size = 4629217, upload-time = "2026-10-21T19:14:00.971Z" }, - { url = "https://files.pythonhosted.org/packages/db/d3/95332d026013398e9bf6edb608305cff4070d95aeeeadec6217018d4e1e6/sqlalchemy-2.1.6-cp311-cp311-win32.whl", hash = "sha256:69eeb530ce74be5e202a98ce35aa58b0776c24e64923c4873c25fa56f8e423bc", size = 2387737, upload-time = "2026-10-21T17:08:09.582Z" }, - { url = "https://files.pythonhosted.org/packages/61/78/ed22f1e9f3597b004da38e853dd0c443908e19e74ceee623e45ed18b967f/sqlalchemy-2.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:f0f22627d6436bd120c6dde0936b12fe75498d1e2e9214ec9863be17f9b80997", size = 2438496, upload-time = "2026-10-21T17:08:11.502Z" }, - { url = "https://files.pythonhosted.org/packages/12/88/d0ccaa6f65d4de6929eb94b1fc474f1b82175b7251242d72cf76d77823a5/sqlalchemy-2.1.6-cp311-cp311-win_arm64.whl", hash = "sha256:8f9d12a8be271c7a83ded1cb7d85921f1b7f65c655c8dae972130a190211bb57", size = 2403090, upload-time = "2026-10-21T18:48:30.486Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/33a77ef74758261e5a2206456713e43d0a72c98ef431fe68fa88ed03b428/sqlalchemy-2.1.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:80009748a5ce0c51a9947542935f9709a1c48b65c69f4a1e56d3072f99e750f2", size = 2474582, upload-time = "2026-10-21T18:34:56.139Z" }, - { url = "https://files.pythonhosted.org/packages/07/05/45e6e2bae7c11e7dbaeb0e9205d92b1b12583b7e199eb2503a7a603f3b9f/sqlalchemy-2.1.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b5885ad1226c8eeea6aeabb4c4036e43202e1bd70cc7a50ece98bcd6ebe5566", size = 4676310, upload-time = "2026-10-21T18:51:19.131Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/8fbe61673b1bb9f5e98d9a9b28455d408ed8bbf6efaf0b03db13357c763d/sqlalchemy-2.1.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e4b4dcf85d7f289def0e3167f15aaad101b7744a065fb2019476edf306712c", size = 4728961, upload-time = "2026-10-21T19:14:02.762Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e4/642e3dbd106866d7f726b7c5479f9c9539f1cf010a2e7b8fe17ee8b8de43/sqlalchemy-2.1.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c314bf9026d865c2b2f67f3b853b4e2edcbe7ed46b67ef11b4a40f0af6aa17b8", size = 4377450, upload-time = "2026-10-21T19:10:58.992Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d8/bcaf98d9f1ac842df7287003a0db36c0f33af9bdb07f1f716d8ab8d6c709/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35348ffcbf686055ea7fd02ba4eb76060ed76761648720c63170c5c8bccd85de", size = 4599619, upload-time = "2026-10-21T18:51:20.955Z" }, - { url = "https://files.pythonhosted.org/packages/44/ac/13226c429b8a87890433fb7dabf8a8b15725e8efdb2e673fdda336bd61cf/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:eafb738fbcba39d12543730eacb65e2d5d627965959b3cc6bd6f797b2d52cc0f", size = 4370497, upload-time = "2026-10-21T19:11:00.827Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5e/d9330f73b6e39f08baed5bd6ea410fd8ee78b8bb1549d0d6a5f555a73db5/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9b197f5d2bcd61b939d2f04b77e86ef24bda266518f3e1526bf666deb20d3ddd", size = 4676118, upload-time = "2026-10-21T19:14:04.705Z" }, - { url = "https://files.pythonhosted.org/packages/62/a3/811e0702c62666d222cae8e7ece846661833896ed8a4c1081840dd49a513/sqlalchemy-2.1.6-cp312-cp312-win32.whl", hash = "sha256:991fa7c6756505e3dc5aad678db5fb0110d9816c2185bc3e2c520de46cfc8c5d", size = 2384827, upload-time = "2026-10-21T17:08:13.393Z" }, - { url = "https://files.pythonhosted.org/packages/06/ee/5609e83ebc921663b3250d053a61b355db72ad3cbbde73e7105024e8bf10/sqlalchemy-2.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ea80b33e57f918509eb91cd074324bff0f9aae40d238121cdc369f449c20649", size = 2437524, upload-time = "2026-10-21T17:08:15.047Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/5b3164be28000eecb4629c07522686ea0fa2a340ebf529801fc861518469/sqlalchemy-2.1.6-cp312-cp312-win_arm64.whl", hash = "sha256:84b359a458611d40061f536b1363733b0af7a71425e4b17d0abb7e543d00e979", size = 2396919, upload-time = "2026-10-21T18:48:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/d7/db/ecf1cf1f5c8cb7661081dcb0561633c174213d7af5fccc17ab48cbe9812a/sqlalchemy-2.1.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0534d3f41295a74d9fe2e0838e06ce6329a0855b2965df5f5d1c4d3f6f1406d0", size = 2468626, upload-time = "2026-10-21T18:49:33.602Z" }, - { url = "https://files.pythonhosted.org/packages/9b/98/1cccefb4fc94f4efd621604063acbbea4e3d530c27151fd7249d68adabf2/sqlalchemy-2.1.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:650f97996a4c529f4930969f3ba11bcdb57ff160ed1cb87ea0174393bf4f5f73", size = 4602545, upload-time = "2026-10-21T18:54:18.766Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/1817d7406ed92c8647a1d1db965a1bc2dd5a3f80d18e4d28ad62daf9bd72/sqlalchemy-2.1.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bada6bd29ae58a0581bf6977c0a43dfd67388fd09129f1475c37eca370cccd8", size = 4655491, upload-time = "2026-10-21T19:05:20.434Z" }, - { url = "https://files.pythonhosted.org/packages/41/5c/0bdaa1676fb182b93e3c87c09cafd08a734ef29820aac6312b87a88f8537/sqlalchemy-2.1.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:96f5fcdfb7e89b6d895e2ddc165f67b3126a585dfdf2a95b0c1f3fcf3380d7d4", size = 4319667, upload-time = "2026-10-21T19:23:16.637Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d7/319e5a049b6f5235d1229858972abb05b8eae2fdd6639dc7bdbbc9cdb68d/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:015bd7b0687b5feaecb9988bc2f36ecb39daca83bd806b094b43b19ea503b0a9", size = 4525975, upload-time = "2026-10-21T18:54:21.553Z" }, - { url = "https://files.pythonhosted.org/packages/19/3b/6e8e5c14a3bcfae59a081a27f33e4f225df8140daab8e90fc3c42afd0d0e/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bf883ae772971bc6155cb64cdfb9e31c50a2099f3b7c8b51b0401bf608a9fe8e", size = 4320900, upload-time = "2026-10-21T19:23:18.835Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a7/7ad220d5ea6308a9a32395785f4ce8beff4d00b0ae56ab9f2dafcca28743/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ff897d13de613768123dee68474956a51fe641c42bba7af6cb7776716a8dbcb", size = 4616756, upload-time = "2026-10-21T19:05:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/08/81/53a198f5a29f247adc68c0c6da69bd2c7bdd89d3ffc58f9e626ee3160f18/sqlalchemy-2.1.6-cp313-cp313-win32.whl", hash = "sha256:9db8bacb2a69b4b5388bc1439463170af28f4cc710933085b81d1658d931d2c2", size = 2382042, upload-time = "2026-10-21T17:11:33.968Z" }, - { url = "https://files.pythonhosted.org/packages/b4/87/69e86a23cd9ccef0d654c09dc69dfea6d62b1311d2f13ca3daab1bcd922e/sqlalchemy-2.1.6-cp313-cp313-win_amd64.whl", hash = "sha256:b9f0b92b4a88b630f1e67df816b43cb017bb583479917dd100541d4f490a9517", size = 2432597, upload-time = "2026-10-21T17:11:35.514Z" }, - { url = "https://files.pythonhosted.org/packages/98/90/a7f81395135cc5da724403ece4340f7e04c1ce752da15d0ad337591ec9c9/sqlalchemy-2.1.6-cp313-cp313-win_arm64.whl", hash = "sha256:bc495d00f235c3df6f376187a4ef48a56e162fdcc0a113ee34518332aa1aa2df", size = 2393224, upload-time = "2026-10-21T18:54:18.405Z" }, - { url = "https://files.pythonhosted.org/packages/d3/23/1d808dee65ee9a7fc0013d3f42af0655bc1e740c55b2ee8a2dfedc09be26/sqlalchemy-2.1.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5f9400b1234555b9ae78450123dec7aaf045d8d978a1e94967437ebe561ae120", size = 2471756, upload-time = "2026-10-21T18:49:35.133Z" }, - { url = "https://files.pythonhosted.org/packages/c3/33/f3feba2de61a20df6293d7da26eedabdd5fd01bb44f6b6a6a192a5a902de/sqlalchemy-2.1.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d106c2191a3a0dea6873c96ab909f31c13978913714a25023f492e9ccc8ffcc7", size = 4596061, upload-time = "2026-10-21T18:54:23.586Z" }, - { url = "https://files.pythonhosted.org/packages/2c/40/42ccf6f7f6adea241f51f236868b8f27076f072b22e194b197f1bafedd75/sqlalchemy-2.1.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39ca3a128260fb3aa1c33554f1993bc5ddf3f82375697028c49e7b411ab80b84", size = 4625648, upload-time = "2026-10-21T19:05:24.248Z" }, - { url = "https://files.pythonhosted.org/packages/43/52/2007c76766751730e4ac51d5df9aa22be8c82a242413085f3a424d7efd7b/sqlalchemy-2.1.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8c34c200a124a1a5769671613b3fc05a21d0a50f7ad942cf0ee12d4390a112c", size = 4319086, upload-time = "2026-10-21T19:23:20.337Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/dd2b47ab0cb726abe54cc365a1d0bdf1a0424d936c740512173eaec7ab89/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:17bc5f508f5e5cee2722a5a14af0a48768b40e1fc5d1bfc92abf6ac587636bd7", size = 4525360, upload-time = "2026-10-21T18:54:25.782Z" }, - { url = "https://files.pythonhosted.org/packages/07/fd/6499902fd0fcf1dc078efc3d33c60434c55901c14dcf20bad98fca1ed556/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c757e13b63a25d59a2566c9b2e77adf14d969207a375ca82a9b4fa1a04b37845", size = 4320430, upload-time = "2026-10-21T19:23:22.497Z" }, - { url = "https://files.pythonhosted.org/packages/5a/22/d805c0b3ff191af4e3cf8b2303c0fca44f5845d019f694c22b2d05e4a3b0/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:73aa8d2f911a54d1310e8258a752207e2d7624f4db46e2320d734fe7218320a1", size = 4586042, upload-time = "2026-10-21T19:05:26.135Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4f/015929abb68561990262348bd18aec9768ad743c293e77ad659947c20fa4/sqlalchemy-2.1.6-cp314-cp314-win32.whl", hash = "sha256:1927eabf26265ed962627e7a64626d9058da29cc0faea341091302ec108ce305", size = 2388927, upload-time = "2026-10-21T17:11:37.139Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1b/19bc3e0b590228bd122237bf38092b292c04e6501f9de7024a5b6d68cf10/sqlalchemy-2.1.6-cp314-cp314-win_amd64.whl", hash = "sha256:fd1154c6394892e727ff234273f5eb2b4319e5714917b016602435d77add8dcb", size = 2439897, upload-time = "2026-10-21T17:11:39.092Z" }, - { url = "https://files.pythonhosted.org/packages/6f/27/e01e46d55b60be7df1420604ad7d81ede3fbf5d41d9cdd7d2d25d5d98b6a/sqlalchemy-2.1.6-cp314-cp314-win_arm64.whl", hash = "sha256:10a4fe106adbb46c9452f750d8a5d4013d9a7e9752f63631c3b255c062b748d0", size = 2404309, upload-time = "2026-10-21T18:54:19.619Z" }, - { url = "https://files.pythonhosted.org/packages/bd/71/62984152c616b26883a686cf05d650f34782108c26c17d0946683b2f13c1/sqlalchemy-2.1.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea5f3dd7450669acbfee7813d3a2a13876806b1d9e4c1626a7bf587c33c68472", size = 2511148, upload-time = "2026-10-21T16:53:10.732Z" }, - { url = "https://files.pythonhosted.org/packages/19/cb/fc1fe57bd16a0b3d4ccf8bffb2b664b3b9fb53ca87e38873413bcec3ee74/sqlalchemy-2.1.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4e4782514c16753f8bce3dd925cd37d26c847bead06e179af6fa4993ce83e88", size = 4927691, upload-time = "2026-10-21T18:59:13.924Z" }, - { url = "https://files.pythonhosted.org/packages/63/79/f155003077cee0893aca3f573e20d69ee28a6219966265b1be55c36a6f26/sqlalchemy-2.1.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da21b36d60f47dd04042e9372ffb3d1ededf5c60a76d43f8c3592221fb2d41cd", size = 4838397, upload-time = "2026-10-21T19:17:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/90/e2/f2787823ad943dbbdf6ceacd3a418bc5b633229e5f2d1d438eedf79a722b/sqlalchemy-2.1.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:048d1beb9bfd7ca1afbcfdb8da9519712827df6cccc80a225715ce387d37bb3b", size = 4516544, upload-time = "2026-10-21T19:44:40.89Z" }, - { url = "https://files.pythonhosted.org/packages/67/9f/8c9eb3efec0c5909d7161c9c4039f9e0d108cb10b2e500f9191f3226a5b5/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8de444335e3723959913f6635d579276a3517b8c19cbdfaefb04e73b5480e88b", size = 4783970, upload-time = "2026-10-21T18:59:15.941Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4f/c80016c99d2ede933088a68dc2b198e1ba09c574d82213703ddd91962349/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:231d0fd85866f847c34c915116c386c83e677ab6f79a94c835b7ae7313b86095", size = 4516656, upload-time = "2026-10-21T19:44:43.492Z" }, - { url = "https://files.pythonhosted.org/packages/fc/72/385e0312720af964a4d72f4a37f625fc966aed4115ea249db202521adb1c/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf937348d4052eac6cfa93c809ad214be75b1f057e3fc0dd2e374a814a23a16b", size = 4775302, upload-time = "2026-10-21T19:17:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/3d/46/2e2eb1f296e4617fb11fbf12acc4aaf1be4e75dd3eed8693bc9967c596ed/sqlalchemy-2.1.6-cp314-cp314t-win32.whl", hash = "sha256:bc8b805fb04472e0f9cb3bc6aad90f24b202afc274b7b8bbd48042130e82997a", size = 2447775, upload-time = "2026-10-21T18:11:52.675Z" }, - { url = "https://files.pythonhosted.org/packages/5e/80/9bdce178d771f569cf3c88516cb7a71eaf6d701086fa47fe5bfb2b3022b3/sqlalchemy-2.1.6-cp314-cp314t-win_amd64.whl", hash = "sha256:84033cab9fec2a515a62d76a7b4acafa83339917db9ea911b65b017928ae7dd8", size = 2515411, upload-time = "2026-10-21T18:11:54.512Z" }, - { url = "https://files.pythonhosted.org/packages/f4/00/587dcab13fdb2ecc5854171a31eaa9ef5fc885bd1b034aa8d7e6ceba19cd/sqlalchemy-2.1.6-cp314-cp314t-win_arm64.whl", hash = "sha256:96ace18758136905f8d5588ec35d6ae4fdc29ce4e4902518263ca33abf10de04", size = 2433508, upload-time = "2026-10-21T18:48:27.552Z" }, - { url = "https://files.pythonhosted.org/packages/34/da/b04b7e7da0374d8e40a6d894fe9969a4d7477d4b5963c94736e85599d27d/sqlalchemy-2.1.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:89d05a1ef531a1fee86d68e731c428b773bd37aabf743bf63dc3709c86f7254d", size = 2471694, upload-time = "2026-10-21T18:46:27.122Z" }, - { url = "https://files.pythonhosted.org/packages/67/17/cc5203117626f71ab3a9ef2fee4cc5ec5c53a80d72b1dd791bc3d497afd2/sqlalchemy-2.1.6-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e12763cb2a400879c6dfe34995a032f707de635c8cad14d6ba777a1250be90b", size = 4599858, upload-time = "2026-10-21T18:55:38.095Z" }, - { url = "https://files.pythonhosted.org/packages/be/bd/a4d24b125b92bb156fe2aa3ce8894d19d7d015e246ce88baceb724776a26/sqlalchemy-2.1.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e1a9ae71366164800942c477e663b31f3accef55d2e039cb7919fdb54b0086", size = 4635165, upload-time = "2026-10-21T18:57:40.67Z" }, - { url = "https://files.pythonhosted.org/packages/21/12/34545d002189c49f05121ecc2b393bd2a5d2dbd9dc0c6e4510c95e3284e1/sqlalchemy-2.1.6-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9bd573f8da863781ace89950d57a959397d5048b1cf69931c29b2b4ce1405bd7", size = 4341217, upload-time = "2026-10-21T19:17:05.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/bf/491ed3e97d1625245c153c083064f36b1c3c1ecc1b1258a84a1f67f80f98/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0d0424a4189f95886c78b48bb500a1dd7ae6dbae76deb552b15813dc298d9c8f", size = 4528088, upload-time = "2026-10-21T18:55:40.234Z" }, - { url = "https://files.pythonhosted.org/packages/10/7f/8750ec25961075140def7fa15f3d07d800a68bacfdbc90112387345b78ca/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:cc60fffea328f38187a4fce43f99c6bbf930de2b87b78251f43f2ba64a1af622", size = 4340063, upload-time = "2026-10-21T19:17:07.687Z" }, - { url = "https://files.pythonhosted.org/packages/62/5a/a9c53bb9dad2732de73d835f691784a3153b4ed693fa385534d2bba39c25/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c390f76f2fe408aa1a3121f3ca3d5c15698e14d913197417355fe3dc016f787d", size = 4597401, upload-time = "2026-10-21T18:57:42.376Z" }, - { url = "https://files.pythonhosted.org/packages/18/02/bc6b004cd8e5906efca1dbbb4e158aa2efe31a5623c32946ec9da73126ae/sqlalchemy-2.1.6-cp315-cp315-win32.whl", hash = "sha256:79c1c20dffd4d8d7e669f096c88f261da8ba3646939cd3eb6faeccb92bae99c3", size = 2388678, upload-time = "2026-10-21T17:51:54.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d6/13087d9a1ded5bf8ccadc2f315c157d4796828829b2c2d6cf1031f1c00ba/sqlalchemy-2.1.6-cp315-cp315-win_amd64.whl", hash = "sha256:f1e78019147ff7e08afaffb88fc3a0aa795171b147eacfc7ff873ce7a0d37dff", size = 2439956, upload-time = "2026-10-21T17:51:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/6d/90/263b259d872cf4e11c7bcd1b278256096482db06fa23ddbc8c00c9918f3d/sqlalchemy-2.1.6-cp315-cp315-win_arm64.whl", hash = "sha256:3974e33fc322b645eedeac8a3dea93022466fee41a6385c744a6656aba293cd4", size = 2403996, upload-time = "2026-10-21T18:50:02.367Z" }, - { url = "https://files.pythonhosted.org/packages/04/60/b4821fe7a42405d12abf9ba48e13b5e056e428d6f0e48c8166f0c7df61fa/sqlalchemy-2.1.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e269c68ec200538ae3fa7f9e8d8ed2b4b34d974c25945286ccc2a488438acf32", size = 2507298, upload-time = "2026-10-21T16:53:12.887Z" }, - { url = "https://files.pythonhosted.org/packages/49/e0/4feccf4511123d55e26269aa22c426ca2cdba88298bd65781bd3609e0e1b/sqlalchemy-2.1.6-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25199a1b499da643de52ab73e972eb07e55e2a95bb99eaa491846098fa2214c", size = 4905036, upload-time = "2026-10-21T18:59:17.626Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fa/7329c40698034056c924210eaba019f46f2914783c3d52b29c47806399c9/sqlalchemy-2.1.6-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:641a3742dac4b8f325667b585097de0f1d20eb3498506c92d5f04c20b3a6c823", size = 4854715, upload-time = "2026-10-21T19:17:36.904Z" }, - { url = "https://files.pythonhosted.org/packages/79/66/098e0981ba55bf61a4b85ae904714987640a7f489da57641c8b6f7d1cfc6/sqlalchemy-2.1.6-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d96186af967d7ae3f76d1e9a8fb70b5beb0f0dbdacfd3b418a946fffbb8781ff", size = 4502934, upload-time = "2026-10-21T19:44:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/20/69/f7ef99af2f3b17e45c1189176decaa2be595848f67827dd7c665ec8e2166/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:5a9b4cbc5f5cda3c9429e1ac503a57f3b95c59886f26af6ffb44e2e7628b7df8", size = 4760666, upload-time = "2026-10-21T18:59:19.419Z" }, - { url = "https://files.pythonhosted.org/packages/72/d7/3dbbefd2342237fbce9b9169ac4f425d039e97b8a9f765467e4a0319c9aa/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b7ddb305126d34fda64c3fe4632a43fb9b724da6cc7d1dedd8434e6c783b285b", size = 4502218, upload-time = "2026-10-21T19:44:47.119Z" }, - { url = "https://files.pythonhosted.org/packages/a4/22/07acba0614b788f94c655a25d76188591921a459fe4f4925a3176b576328/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70b435f719d7156623f49ac6539f30dd8471500b5acf399f3b318c1c17f6fa7f", size = 4780103, upload-time = "2026-10-21T19:17:39.4Z" }, - { url = "https://files.pythonhosted.org/packages/59/0d/ea88e6e0e069b6c1c65c7a32043ab2203386c80d0bd1a25cf1ba39007741/sqlalchemy-2.1.6-cp315-cp315t-win32.whl", hash = "sha256:654192c9f192459ee5f63e6606cc6dd8c50681c28e0689d2a494fc9522b357c1", size = 2444581, upload-time = "2026-10-21T18:11:57.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/25/2182431a4955adfeab69a89e7e4e81eaabbfbf1846ff8634b0f9448fd6cd/sqlalchemy-2.1.6-cp315-cp315t-win_amd64.whl", hash = "sha256:a053a9d9c9ccc9b72d90ecdcbabcbe57446defc7e8c52fc9978188f78742ac19", size = 2510428, upload-time = "2026-10-21T18:11:59.23Z" }, - { url = "https://files.pythonhosted.org/packages/b3/69/e290384d311e6e2c495734673bdbd2e358dc6725e7c1525326eb8e34af8d/sqlalchemy-2.1.6-cp315-cp315t-win_arm64.whl", hash = "sha256:61dc3fa488208a2f03e0e25ad32950dc024545291acc7348f2e550ea1bc5abb4", size = 2429115, upload-time = "2026-10-21T18:48:28.987Z" }, - { url = "https://files.pythonhosted.org/packages/c1/69/c75534e336b06b3fd3f76de73d89736510800e60c77c58d4e4ce18389018/sqlalchemy-2.1.6-py3-none-any.whl", hash = "sha256:106878cf3795cd3d357e2e858609f9e44f20f19b2d8e48a3532bef3f90b03fbe", size = 2063055, upload-time = "2026-10-21T18:42:10.234Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/08/bb/28e3106ee586e5b2f5ecf211eec1348055f0f926314b13fe23b9cffa9039/sqlalchemy-2.1.5.tar.gz", hash = "sha256:0df76cf1ca680ad745bd378a6299a058611a8a784eeaeb4ed78ad4a0b7ce2003", size = 10569593, upload-time = "2026-10-16T20:52:20.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/78/d4a0f4e7432f186d3082e5f1b8573b6776dd15bda01ea2149f540c7bceda/sqlalchemy-2.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:116cad2685f16398c3486e30832e07197a7e12147e465a44a187c250059f23d0", size = 2468654, upload-time = "2026-10-16T22:45:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/34/1d/628915299f551b0a4e2e2492975e39cf2688288fc9ebc4ac8c1483d89bd0/sqlalchemy-2.1.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bac60969b1a96f8ee4db82c647a3370860f76b2a41dd7b95f578a8421c7c87bb", size = 4633894, upload-time = "2026-10-16T22:34:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/92/31/78d92c64da26adc3febcf98722426c1090be70b3ac54ff1573a5afa36aca/sqlalchemy-2.1.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:926c61c589c3bebb6c540ba0b60fd9a2e8eaf6b5ae9e3ec94cb2cbb2fe10a022", size = 4656153, upload-time = "2026-10-16T22:40:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/a254f76935dfd5d15ce9e3b3cd306b26670bd9473fee1dcd80ad816790d9/sqlalchemy-2.1.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46a50f5d226e9321970867c91252a71485301b67eaee2f76db92b2f6bea5fdcb", size = 4386171, upload-time = "2026-10-16T22:52:43.185Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/34c21248357dcbb68122d79d68126824d15a70448c5e7719266789020160/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb7afa85837f56cd07235dbb05858f5137d5fe7a1e813b12669a52462a1b6ae5", size = 4571827, upload-time = "2026-10-16T22:34:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/4e/11/18ee59f913c99fa5c991d621faef6f1612bb70260af98d856b80e680170a/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ad305d5fed915340464441dfed47086303936288ed0a5ebc1a0b2ed41678fa41", size = 4383076, upload-time = "2026-10-16T22:52:45.106Z" }, + { url = "https://files.pythonhosted.org/packages/5a/aa/f45c4f56004a2596576cf02744adc9074ac945e98e49946551e07c57afac/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ca1b380f940fbd94c1df94d7b502b9c496fe9a62f5200e0c6feaa8db7175401", size = 4626378, upload-time = "2026-10-16T22:40:51.987Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/842b0416ffa5d0e0ce5c25a7b52ac31d4b1bd55e7351cfd9e7b0e32273a7/sqlalchemy-2.1.5-cp311-cp311-win32.whl", hash = "sha256:4c4666b9884e4a807b27835ab236056278915ea9549a2405e83d86297e7ba0da", size = 2384833, upload-time = "2026-10-16T22:33:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a8/6efc290e861ce6fd480e5483ba0bb5af15441ca02617e09aae648b333049/sqlalchemy-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:9edda6c6e22113702d9603b1178f1ab16f5120b49995134b407f11ebb4446b91", size = 2435596, upload-time = "2026-10-16T22:33:39.743Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/0e4241d71dc7b99aceeac0a841ae41aad24e3ede04eeed2d3c27df06cbd4/sqlalchemy-2.1.5-cp311-cp311-win_arm64.whl", hash = "sha256:29f47da90629119646ef90c52f6185131a10b6ed811b26490b01c31a9e9b2453", size = 2400190, upload-time = "2026-10-16T22:27:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/35/a5/286dc0a04b2f2052b15785784db16c39628d3d7424b25112a306bd0e1d43/sqlalchemy-2.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3eb1907fc67aacc160276e7f61c1f71a7a38c10e84ff87b74d90cae0abd137bb", size = 2471746, upload-time = "2026-10-16T22:45:12.171Z" }, + { url = "https://files.pythonhosted.org/packages/44/80/066b7dca8250d8a6135afe692b8a80bc5239c0a0344213ba847fc3f9a46b/sqlalchemy-2.1.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22eeeb297ced15cd7571aca242aede6a59141dcc3282c29c987fbc83f05c823b", size = 4673470, upload-time = "2026-10-16T22:34:28.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/33/8be720a412d595fa061a9ddcff6b5340ae09d3317ba6a32207523eb239f3/sqlalchemy-2.1.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb2fc3c1e8d2498d944b6ff64d32a63a5c900a14892c200626e701e69187113", size = 4726119, upload-time = "2026-10-16T22:40:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/938538f2d7fa2e1ba62d07beaac3303c9ec0d21479e35320cbe5b0312ccc/sqlalchemy-2.1.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a534a39a9247a17843d9f6e34a0877d1a2af870b273e53e1c453b3a65f93c28e", size = 4374608, upload-time = "2026-10-16T22:52:47.037Z" }, + { url = "https://files.pythonhosted.org/packages/35/e4/b27d25f452994dc9acecb86f7984750c0f7891734fc1a50aee83483fd88e/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cc3292040c980e9f1a54b14da76cf938a6a04a27e8acf6cb7709e9c3a40935b", size = 4596779, upload-time = "2026-10-16T22:34:30.811Z" }, + { url = "https://files.pythonhosted.org/packages/53/51/f2fae2a6bb734400aec41f3e625efcc03f1e43a26b852aa89206efce61a1/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ce8d3de4e0489671fa023dea3f769360df02b2df6e482a44f8fa4421dd41c0ae", size = 4367657, upload-time = "2026-10-16T22:52:49.12Z" }, + { url = "https://files.pythonhosted.org/packages/0a/22/05957d18ac378d20ee048010916d3c1931442bcbd0c1c21b18e0e868ab6f/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:143cea634f03f9a8009fdeaeab46808031ab05bf0c50ec0aa25da51c29bdeeb8", size = 4673278, upload-time = "2026-10-16T22:40:56.847Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/266829855add33a99f1cb59d548e563e208aea514277d2f9650b454041dc/sqlalchemy-2.1.5-cp312-cp312-win32.whl", hash = "sha256:7133bd0ac969ba1a108508975e0c26229329cadaaa48844f0a038dafa610693b", size = 2381924, upload-time = "2026-10-16T22:33:41.207Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1b/3f59712d0a7e08bf3a075df43b0a3d89de051728d829d6836f546c533012/sqlalchemy-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:ee7267d7ffed80c370dfdd7ad1c741d0227f0e8054b83d00715fb99307bd330c", size = 2434617, upload-time = "2026-10-16T22:33:42.917Z" }, + { url = "https://files.pythonhosted.org/packages/57/f6/31e94eb5403ca04d393805fe6df5470f48aabdbfb05437e93c2a7f8e3ebd/sqlalchemy-2.1.5-cp312-cp312-win_arm64.whl", hash = "sha256:b3732a60b01560c9efee3b9ea188dc12614b2642ffce4f8235c4fbf72dd40fe8", size = 2394015, upload-time = "2026-10-16T22:27:33.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/aa/f10fc142d2db255249107ee8b21bfbabc10b90f7fadf13c3cf898acc6179/sqlalchemy-2.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc1c86db97372e6bba18a7bcf0f6150dbdd808237f0f52802ef0bec676a94642", size = 2465792, upload-time = "2026-10-16T22:42:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0d/d395e310e36b5bd39a121cd455504aa1e9111f419efaa88be6b814b4e54d/sqlalchemy-2.1.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c229afac414d691d2f69a6f4e294ac5b5fd334c49da1f4fb86ab7f2a466271d", size = 4599703, upload-time = "2026-10-16T22:55:25.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6d/04f4ffc5038571e712c4f88f20d7022091bafab082e85ea5b37a7e8270f1/sqlalchemy-2.1.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cf433bcb77696cc915552d7f1e8d9117a5dae5aff9fceb06a69d74b3b3c2bd1", size = 4652645, upload-time = "2026-10-16T22:59:23.273Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/08bc336576755daf087e0888e44657f4f72b6676a99522e7a612f0c2481e/sqlalchemy-2.1.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:399d3e212cb557d5bd1ca6f79981e8c3f45c0f6be60b63a9ded790396a81af08", size = 4316833, upload-time = "2026-10-16T23:15:10.011Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5c/476441255cc7afa25e8c75e5a61d3c5c3af7cf61c951ebed2ee9873ec4b1/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a51c56afdb880e9f83736a2e81c02dd002fe1c5ad68346a35d97a2e135b1568", size = 4523134, upload-time = "2026-10-16T22:55:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/63/ca/064e5e3ca7c8f256c8f9b5206eafd410a39226c2ab6fe7e8c1e70725e432/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:17f8c68bd35bb76b2d9ef1097941fb783ef9ef6604c1d2a72c2e276430aeffca", size = 4318058, upload-time = "2026-10-16T23:15:12.139Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/e38956834896e21038a67193f91b189b9a61555b92fac42368027d5a2524/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:59dbd7412531da0124df8667a244b5155773632e9bd79a5c2ead393b11b59de5", size = 4613916, upload-time = "2026-10-16T22:59:25.19Z" }, + { url = "https://files.pythonhosted.org/packages/14/b3/5531eb9d10d4bd38bd7e6c505776ac73be259dc117c9f07a8c46db4e9a2b/sqlalchemy-2.1.5-cp313-cp313-win32.whl", hash = "sha256:3f566787cb3a6a25ad8c5170e90167d4e8ea103d412aa4ea95021a6df0b95964", size = 2379136, upload-time = "2026-10-16T22:48:37.111Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/c48924aafae616d59a2eb0375feeaf4bc4356327a442bda3b9205861b543/sqlalchemy-2.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb8dcd5e1cc12fdd4d490819ec7a56e8c02e86ca9087a472e28b422ff932bae0", size = 2429688, upload-time = "2026-10-16T22:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/51/84/0855a7d42b700b091af6f817c171c2ff97478b0d5143fe61963adbff9201/sqlalchemy-2.1.5-cp313-cp313-win_arm64.whl", hash = "sha256:65eece9d9494e46de1eb3d49eb438acfcecf059e7a92a2f3daf72ec26d371757", size = 2390320, upload-time = "2026-10-16T22:31:43.871Z" }, + { url = "https://files.pythonhosted.org/packages/01/b0/59b26375cd618e861a9295d855b866ca85be654d20ed47e494e33c3bf26e/sqlalchemy-2.1.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5541a110ac4bb6377c785aea1b8f0b4d6658ed0ca5061ed1115d0725062b026b", size = 2468925, upload-time = "2026-10-16T22:42:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a1/01931ba38770f169ce70e4034b0dcf2c1a776fe7d8dde93a34b6355510b1/sqlalchemy-2.1.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f029b2f7114623c03f762596f313515a2358004c44eb18924a15940f2e27d30f", size = 4593220, upload-time = "2026-10-16T22:55:29.253Z" }, + { url = "https://files.pythonhosted.org/packages/42/9e/fa478482503a69dfb3b0cb82f3f80fe3816026530dd34b5359e2acd9f30d/sqlalchemy-2.1.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc44dcc80e89a006af415edffc7409b1f2976a4597065449c166987f95801ab4", size = 4622806, upload-time = "2026-10-16T22:59:27.153Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/357ff85423013de8316316752c36a6bd1b7f3830c30dc63754afe06ba589/sqlalchemy-2.1.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:72ddd7a8ec187d02de0ef316a15aaf381ddcc594958247b02a3dcdb4c42f0b02", size = 4316247, upload-time = "2026-10-16T23:15:14.27Z" }, + { url = "https://files.pythonhosted.org/packages/43/53/88c5af325c81c929d0ed5b7ec15b087173bd3587d93366c5e336b9647267/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f5173a6e60a15090f9e2348a8ca94cc13de72796c16733f3b62fbb22acba07b8", size = 4522522, upload-time = "2026-10-16T22:55:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/87/29/f6f01f121d5e590c568f405f78b8eb3edace446c114126fc428536446dd6/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:09b10bd68223d57cce39c08947efae3f91ac40b552e07c7c869c3350a0fc4c23", size = 4317593, upload-time = "2026-10-16T23:15:16.397Z" }, + { url = "https://files.pythonhosted.org/packages/0c/6f/14863fc9aea433645851330242823b1879cdaac71c118f7c9366e7caf4da/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ea4cb5022462d9f15e7b72a52c267385aa97ac224c8503ac63ba90c0067ad0d9", size = 4583207, upload-time = "2026-10-16T22:59:29.322Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e9/6907fc009d9d06e8d03d6bf286df46e878ea2a85c839dcb65ef11ceb3a39/sqlalchemy-2.1.5-cp314-cp314-win32.whl", hash = "sha256:c3ffbe6c680c626403b7e9697d92465501078731b9e8863b1410826e66b950e1", size = 2386018, upload-time = "2026-10-16T22:48:40.692Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/dea47b4b361c427d390edc10d73050e6d11d905c56ef26861ff5f94aa8fa/sqlalchemy-2.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:061d089901319463a1160e4596191e355d2a9b4cce4852dac5adc301b085ac2f", size = 2436983, upload-time = "2026-10-16T22:48:42.2Z" }, + { url = "https://files.pythonhosted.org/packages/be/28/305a91768a1ca0980c6ead455dcda5a2c1e616022901898e966ccf913ba9/sqlalchemy-2.1.5-cp314-cp314-win_arm64.whl", hash = "sha256:f5bc7fb93f1c17efefeb062bc4d2742ad1b71d56c36e6e2898a371ac6a56839a", size = 2401394, upload-time = "2026-10-16T22:31:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6d/d4bab269d94ed1017950873643b5bd3eb271a5b8d5b0451250f0173f90fc/sqlalchemy-2.1.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9bb993b1d0890a2da90589684c3d09de75df82c06aa8f7eda246c7cd0ea3e820", size = 2508315, upload-time = "2026-10-16T22:55:01.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/80/aeab72b51fceb71b1e1d62cabc6f51c9d8f20588165aa54e0d0d6cc77c05/sqlalchemy-2.1.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df7e71ed051e4003e73484ef6547aacfb3a2322aac0c230d530c97e48e77b4d7", size = 4924848, upload-time = "2026-10-16T22:41:10.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/7318ca0ae828fa1605dd4481f8eeecb5325a7ead6289c1206fb1ff1e2e1d/sqlalchemy-2.1.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c76d4cb9c76a6906563133449a76d9eae6c8529ca09484b8615ca604442b442e", size = 4835557, upload-time = "2026-10-16T23:06:18.482Z" }, + { url = "https://files.pythonhosted.org/packages/35/17/7a5a196b0af72d65cbcd3a6fb7c4128693d039faf58a682ffe2d19329420/sqlalchemy-2.1.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59a56be2e4e972061f9d5c1d5b01763b8b2207b48b956fbb6879c7443f5817f7", size = 4513707, upload-time = "2026-10-16T23:28:18.969Z" }, + { url = "https://files.pythonhosted.org/packages/91/50/ca323ed15fc06c73a7fd85a3a339749994c560c12312ee8a53abf0f9a7ec/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc1a61d7f42e71746b5a2e10e90a6b174b4add441b7b6cf14278f1e029b2b800", size = 4781127, upload-time = "2026-10-16T22:41:12.784Z" }, + { url = "https://files.pythonhosted.org/packages/26/3d/c56a75d0a2f6af08f6bb28d0b69e129911a2fc52b24dbafd7a7e5ac5c1aa/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:740e96dfb99a615c8056070d3747efe8bce4c145e5953ccc9e4b004c139de0d2", size = 4513816, upload-time = "2026-10-16T23:28:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dd/84e0cd627bbd3e35b5f033191a4f597e611ff96e424db36a444a8d6755cc/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:92a0bb0595331c8e40bc615a67cc7bca9b761487409d1bf46c598b6ba5c0754d", size = 4772462, upload-time = "2026-10-16T23:06:20.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/cf0a920fc40344f8a1806d661e7c702341aebd871e31e1d1d6e0fbc9d1e0/sqlalchemy-2.1.5-cp314-cp314t-win32.whl", hash = "sha256:11185c72b5372369166cb721537943f8e1e6e42a399e9bac19e02f3ee20592a4", size = 2444861, upload-time = "2026-10-16T22:51:51.169Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/4c5dc7e30fe740a01df4ba5ab800be2d7674b87f042e4c21c7ca89b36f9d/sqlalchemy-2.1.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5173207b8fe1ddfc1d451c420a2a9cbbd0734b4a19896b8080e3744497e1c20c", size = 2512497, upload-time = "2026-10-16T22:51:53.014Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2f/d3e9bfb1faabea302b98ba8c9dd6252bc362c6afeabe28ffcc1eea8a6bd2/sqlalchemy-2.1.5-cp314-cp314t-win_arm64.whl", hash = "sha256:def2f117a6ec0a25202ee36a148bbcfad3d43e2f2bb2e698e6d4448c6db5cb19", size = 2430598, upload-time = "2026-10-16T22:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/17f07378eea7ec2fac8ac60f99fe3c474ef5c19a3adede70ae9886eb82d7/sqlalchemy-2.1.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:a0b80f91d9ed92be7a134278ea2e83a693c07d9986824ca51d859af9e57e18f5", size = 2468856, upload-time = "2026-10-16T22:27:43.754Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c0/4fef29827b198937e09c40d0fa36b9738bd3914f0e5040927dfac8dfee21/sqlalchemy-2.1.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3033ddeeeb34c6c536622c07e932a20f8c9f58d6dd76e56595a76b55a25011ff", size = 4597018, upload-time = "2026-10-16T22:41:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/37645f8445ac3774e18f8dc72e0fa0ac868e0dff0e987ff3bec644f55102/sqlalchemy-2.1.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4d30723171d511ec16ac057872a38b9b395420c0aaf81b2f94dd7bf1345c5fe", size = 4632322, upload-time = "2026-10-16T22:44:53.83Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/15466cd08a6c329acc4f730dff7041e84122456fe38c518c93a07d6d82ca/sqlalchemy-2.1.5-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cefa5badc13df1c9082bacdfa435c8eb6dcb9b32717f51d09d1e160a599646f", size = 4338381, upload-time = "2026-10-16T23:01:22.714Z" }, + { url = "https://files.pythonhosted.org/packages/12/2b/a93b7c8f6886138dac91f66fccd3b42473c365408d9a4c1b69403cf206f8/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2549e503185897de1fc5ca7730d5daed0d27d0b26df4a059dcdd33ebe81b7b4b", size = 4525244, upload-time = "2026-10-16T22:42:01.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/47/d3926cacd194be073a2053a05470f20ed7da623f399e95213c1e497954fd/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:5a6871ee159a943d874012de835d56d0544ee871a99dbae05bc053da315b2c23", size = 4337223, upload-time = "2026-10-16T23:01:24.702Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/b15ab4712342a18fb3949c77687c24d3749e86079c9905cc2cbebbd44da3/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:51e290080415325b66a94d20f6360e310606acb832f2bd677d4fcb95728e7ce1", size = 4594562, upload-time = "2026-10-16T22:44:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ab/3b6ef6c77740d7fbf5a3eb45f95768fb809b0769bd21df81d5f8c002e5a5/sqlalchemy-2.1.5-cp315-cp315-win32.whl", hash = "sha256:c228084025a28aa038aef9b821d842508650fe12fe51f06e9cdaa4393be7812c", size = 2385770, upload-time = "2026-10-16T22:41:04.484Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c1/0b3256bc11487b2846a26d58946caea0ed395fd500951a16fb82d31a20ff/sqlalchemy-2.1.5-cp315-cp315-win_amd64.whl", hash = "sha256:bc09dd22c3fc491a2df7d16ca4167de35573f40c2e82dd4d6ddae589a73e7032", size = 2437043, upload-time = "2026-10-16T22:41:06.12Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a0/09eaa7897f6f745de16220b20c63660948a95716be90d2f405af08222e57/sqlalchemy-2.1.5-cp315-cp315-win_arm64.whl", hash = "sha256:375ef1bf78bc269a220963163d2580d5377898ed23a00bdd98a02d755421573d", size = 2401090, upload-time = "2026-10-16T22:28:58.035Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c2/4c747dcc6fe329dd07ab57233f334eca19a3ff05934e0aba4f25a0977a67/sqlalchemy-2.1.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:7af1c92255cf8d50e87a808ce41096907332acdd09f285426a52f7c300e152db", size = 2504475, upload-time = "2026-10-16T22:55:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/07/f1/183446b1bc29484087ba7a6bb199a265c0559bf8ff201c1ee54bf1fe0936/sqlalchemy-2.1.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85891e0828e76a77fe912995dbc3ee949099488ba0a2ab323535d2e767446b20", size = 4902197, upload-time = "2026-10-16T22:41:14.703Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/b24baf343461eefdecc672df1ccbb14c8c64028a11625dabc97b0bb9791b/sqlalchemy-2.1.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a72e062d2d38826dd89231df692fb40f12d7d1b9d118be23bcc1cf93c6e31782", size = 4851871, upload-time = "2026-10-16T23:06:22.861Z" }, + { url = "https://files.pythonhosted.org/packages/da/27/f688b34ea3d85b38e01b7463874f1f4b91619dfed4e6cac88c27caface73/sqlalchemy-2.1.5-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b79ee943e2e12246f873345762b83dbadd1fcfc3e832eab83272d672704dbdea", size = 4500095, upload-time = "2026-10-16T23:28:23.431Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/d8e13be8d0c25ba49244a9c24579ef61d5a6b57f98e55fbb173b7923ec3a/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:e9418d87b55c469c2f1328246d4533238cdc43134c695d6b9a6b7a87da49d463", size = 4757826, upload-time = "2026-10-16T22:41:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/a0bd2ead949cc605ce3ad5d38e2fe84963b2aa7f89e8648c1bbad13a3679/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:898d86c22640fe52ffe02b4abc043a3c6754b4875837599e739be440966dffa1", size = 4499377, upload-time = "2026-10-16T23:28:25.203Z" }, + { url = "https://files.pythonhosted.org/packages/b9/dd/c1ca0713a1a7d8d6eb5864edd2d11d7a6a327aaba664645733cb53662425/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b3ad20cd035392f2b91a6466e24643990c1378beda80c9bda87a3a41e841e838", size = 4777264, upload-time = "2026-10-16T23:06:25.203Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/9dec72cea689f16bc8ecfcd62b4aa2796009a7cef6be85a9f70310ecf71b/sqlalchemy-2.1.5-cp315-cp315t-win32.whl", hash = "sha256:db34eefaf992c84460ab26ecf2fc2bdcc68965d6becfae8182cae0efeaa3e8f8", size = 2441666, upload-time = "2026-10-16T22:51:54.58Z" }, + { url = "https://files.pythonhosted.org/packages/dc/28/f44c4524ce8a68935bd6f23ee801165072fb8c1e477fc9855efd9eaa2360/sqlalchemy-2.1.5-cp315-cp315t-win_amd64.whl", hash = "sha256:173054351d863ddada188284e309e87a9f030132c8d534e05458971ff061aa1e", size = 2507526, upload-time = "2026-10-16T22:51:56.262Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fc/e90c91f6c2709a431f9b75e41bd31216337859411359f4a4fc2615c989e3/sqlalchemy-2.1.5-cp315-cp315t-win_arm64.whl", hash = "sha256:22c8d0873168a54f614e381559ea83d547b2cc5488b95b049bdc92667586bd5f", size = 2426204, upload-time = "2026-10-16T22:32:56.011Z" }, + { url = "https://files.pythonhosted.org/packages/de/e9/61176e4753f74ab5f6e72a56314ee654d25c31e6fc98c99060442998cccb/sqlalchemy-2.1.5-py3-none-any.whl", hash = "sha256:2f77b77b716f005b2dc3cc059d56df6a919bfb5b439e4149890a9df6c36be9e3", size = 2060214, upload-time = "2026-10-16T22:43:30.801Z" }, ] [package.optional-dependencies] @@ -5713,16 +5922,18 @@ dev = [ { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, + { name = "auto-pytabs", extra = ["sphinx"] }, { name = "bump-my-version" }, + { name = "click-extra", extra = ["sphinx"] }, { name = "covdefaults" }, { name = "coverage" }, { name = "dishka" }, { name = "duckdb-engine" }, { name = "fsspec", extra = ["s3"] }, { name = "hatch-mypyc" }, - { name = "mkdocstrings-python" }, - { name = "msgspec" }, { name = "mypy" }, + { name = "myst-parser" }, + { name = "numpydoc" }, { name = "pandas-stubs" }, { name = "pgvector" }, { name = "polars" }, @@ -5731,11 +5942,10 @@ dev = [ { name = "pyarrow" }, { name = "pyarrow-stubs" }, { name = "pydantic-settings" }, - { name = "pymdown-extensions" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-databases", extra = ["bigquery", "oracle", "postgres", "spanner"] }, + { name = "pytest-databases" }, { name = "pytest-mock" }, { name = "pytest-sugar" }, { name = "pytest-timeout" }, @@ -5743,25 +5953,44 @@ dev = [ { name = "requests" }, { name = "ruff" }, { name = "sanic-testing" }, + { name = "shibuya" }, { name = "slotscheck" }, { name = "sniffio" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-click" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, + { name = "sphinx-iconify" }, + { name = "sphinx-paramlinks" }, + { name = "sphinxcontrib-jquery" }, + { name = "sphinxcontrib-mermaid" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "types-cffi" }, { name = "types-colorama" }, - { name = "types-docker" }, { name = "types-protobuf" }, { name = "types-psutil" }, { name = "types-pygments" }, { name = "types-pymysql" }, - { name = "zensical" }, { name = "zizmor" }, ] doc = [ - { name = "mkdocstrings-python" }, - { name = "msgspec" }, - { name = "pymdown-extensions" }, - { name = "ruff" }, - { name = "zensical" }, + { name = "auto-pytabs", extra = ["sphinx"] }, + { name = "click-extra", extra = ["sphinx"] }, + { name = "myst-parser" }, + { name = "numpydoc" }, + { name = "shibuya" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-click" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, + { name = "sphinx-iconify" }, + { name = "sphinx-paramlinks" }, + { name = "sphinxcontrib-jquery" }, + { name = "sphinxcontrib-mermaid" }, ] extras = [ { name = "adbc-driver-bigquery" }, @@ -5786,7 +6015,6 @@ lint = [ { name = "slotscheck" }, { name = "types-cffi" }, { name = "types-colorama" }, - { name = "types-docker" }, { name = "types-protobuf" }, { name = "types-pygments" }, { name = "types-pymysql" }, @@ -5799,7 +6027,7 @@ test = [ { name = "coverage" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-databases", extra = ["bigquery", "oracle", "postgres", "spanner"] }, + { name = "pytest-databases" }, { name = "pytest-mock" }, { name = "pytest-sugar" }, { name = "pytest-timeout" }, @@ -5893,16 +6121,18 @@ dev = [ { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, + { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, { name = "bump-my-version" }, + { name = "click-extra", extras = ["sphinx"] }, { name = "covdefaults" }, { name = "coverage", specifier = ">=7.6.1" }, { name = "dishka" }, { name = "duckdb-engine", specifier = ">=0.17.0" }, { name = "fsspec", extras = ["s3"] }, { name = "hatch-mypyc" }, - { name = "mkdocstrings-python", specifier = ">=2.0.1" }, - { name = "msgspec" }, { name = "mypy", specifier = ">=2.0.0" }, + { name = "myst-parser" }, + { name = "numpydoc" }, { name = "pandas-stubs", specifier = "<3" }, { name = "pgvector" }, { name = "polars" }, @@ -5911,11 +6141,10 @@ dev = [ { name = "pyarrow" }, { name = "pyarrow-stubs" }, { name = "pydantic-settings" }, - { name = "pymdown-extensions", specifier = ">=10.16" }, { name = "pyright", specifier = ">=1.1.386" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, - { name = "pytest-databases", extras = ["postgres", "oracle", "bigquery", "spanner"], specifier = ">=0.19.0" }, + { name = "pytest-databases", git = "https://github.com/litestar-org/pytest-databases.git?branch=test%2Fclientless-all" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "pytest-sugar", specifier = ">=1.0.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, @@ -5923,25 +6152,44 @@ dev = [ { name = "requests" }, { name = "ruff", specifier = ">=0.16.0" }, { name = "sanic-testing" }, + { name = "shibuya" }, { name = "slotscheck", specifier = ">=0.16.5" }, { name = "sniffio" }, + { name = "sphinx" }, + { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-click", specifier = ">=6.0.0" }, + { name = "sphinx-copybutton", specifier = ">=0.5.2" }, + { name = "sphinx-design", specifier = ">=0.5.0" }, + { name = "sphinx-iconify" }, + { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, + { name = "sphinxcontrib-jquery" }, + { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, { name = "sqlalchemy", extras = ["asyncio"] }, { name = "types-cffi" }, { name = "types-colorama" }, - { name = "types-docker" }, { name = "types-protobuf" }, { name = "types-psutil" }, { name = "types-pygments" }, { name = "types-pymysql" }, - { name = "zensical", specifier = ">=0.0.71" }, { name = "zizmor", specifier = ">=1.28.0" }, ] doc = [ - { name = "mkdocstrings-python", specifier = ">=2.0.1" }, - { name = "msgspec" }, - { name = "pymdown-extensions", specifier = ">=10.16" }, - { name = "ruff", specifier = ">=0.16.0" }, - { name = "zensical", specifier = ">=0.0.71" }, + { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, + { name = "click-extra", extras = ["sphinx"] }, + { name = "myst-parser" }, + { name = "numpydoc" }, + { name = "shibuya" }, + { name = "sphinx" }, + { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-click", specifier = ">=6.0.0" }, + { name = "sphinx-copybutton", specifier = ">=0.5.2" }, + { name = "sphinx-design", specifier = ">=0.5.0" }, + { name = "sphinx-iconify" }, + { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, + { name = "sphinxcontrib-jquery" }, + { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, ] extras = [ { name = "adbc-driver-bigquery" }, @@ -5966,7 +6214,6 @@ lint = [ { name = "slotscheck", specifier = ">=0.16.5" }, { name = "types-cffi" }, { name = "types-colorama" }, - { name = "types-docker" }, { name = "types-protobuf" }, { name = "types-pygments" }, { name = "types-pymysql" }, @@ -5979,7 +6226,7 @@ test = [ { name = "coverage", specifier = ">=7.6.1" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, - { name = "pytest-databases", extras = ["postgres", "oracle", "bigquery", "spanner"], specifier = ">=0.19.0" }, + { name = "pytest-databases", git = "https://github.com/litestar-org/pytest-databases.git?branch=test%2Fclientless-all" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "pytest-sugar", specifier = ">=1.0.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, @@ -5991,15 +6238,29 @@ test = [ [[package]] name = "starlette" -version = "1.10.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/bb/3d8757f21e9a53d9035c9eb039a777047139a16bfdbedfc7a58241c3d914/starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b", size = 2734356, upload-time = "2026-10-24T15:04:12.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/2e/947cfe02def9371c07cd5bf5b6b61c829e9aecb9904db02a66cfafbc1eec/starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4", size = 80571, upload-time = "2026-10-24T15:04:10.371Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[package.optional-dependencies] +widechars = [ + { name = "wcwidth" }, ] [[package]] @@ -6152,19 +6413,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/33/a3449e3e93c43861b92d0a93ed4ea5505756463e507625812cfb29c91c7a/types_colorama-0.4.15.20260508-py3-none-any.whl", hash = "sha256:b0c39908a3e5171ef1f8bf3d59fae082e9eaff3a19ca49b6d640b83f78cff61c", size = 10755, upload-time = "2026-05-08T04:47:13.761Z" }, ] -[[package]] -name = "types-docker" -version = "7.2.0.20261006" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/bd/7c46cbb3190622accb6c6aed717083712b0531c6e8ec5f753b7b95cdebe6/types_docker-7.2.0.20261006.tar.gz", hash = "sha256:bd2fc6c302bce7c0caeb06b6e60f7738085fc15f7f5be540b917f6dd33b36619", size = 37408, upload-time = "2026-10-06T08:16:13.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/67/2f16e88c38b6d873d0d417f81a12af119998ede025f99b1422fb157ad288/types_docker-7.2.0.20261006-py3-none-any.whl", hash = "sha256:866041971a378a226edd095a4a55d3924e51d9d15faea5beeeef256f86f2320d", size = 51408, upload-time = "2026-10-06T08:16:12.831Z" }, -] - [[package]] name = "types-protobuf" version = "7.35.1.20260906" @@ -6210,18 +6458,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/38/e375026fb4ff74fed6bdb84d7d336571d0603b6b9675751579cec84cfc9b/types_pytz-2026.5.0.20261006-py3-none-any.whl", hash = "sha256:9e4a893b362a8eed4e10a348c80603ade65bdb3819419e589364afafe2ea08b1", size = 10168, upload-time = "2026-10-06T08:15:11.985Z" }, ] -[[package]] -name = "types-requests" -version = "2.33.0.20261006" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/15/9b7e2e2e7c87d01366185b198b3febc6bc0c973f2bf21a62da4ab7d3495e/types_requests-2.33.0.20261006.tar.gz", hash = "sha256:0652999e9306aea345f40732d58fa49a7f6cade6a0d74d92119c5c8d82eddaf0", size = 25316, upload-time = "2026-10-06T08:15:57.782Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/72/b82789207b3d360ce9f5a4372c790c52cc3dd928aeb7d4faeec652b651b2/types_requests-2.33.0.20261006-py3-none-any.whl", hash = "sha256:26cc8146505cab33cda9737991929e4144c559bebe05078ccc6998f27c4ca2c1", size = 21445, upload-time = "2026-10-06T08:15:56.658Z" }, -] - [[package]] name = "types-setuptools" version = "84.0.0.20261006" @@ -6362,118 +6598,90 @@ wheels = [ [[package]] name = "uuid-utils" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/02/66f6073ed29a39d546700678238268125cd5c712a0e9b28d5e6a6faa0b4e/uuid_utils-1.1.0.tar.gz", hash = "sha256:8c9c2a6714aab31da915e7b85b376cc21f1f7379db9f1d77828ec72662e60555", size = 43769, upload-time = "2026-10-19T11:23:51.676Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/bd/6b9f538a0ea7c87ed1ea218cbf699f890751001b90fd58544770e81d80f9/uuid_utils-1.1.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7425ea6bdd4edaf062cc4ef69ab5b7ae97ede1ea541e8d9d199edf2ee35e2f6c", size = 569226, upload-time = "2026-10-19T11:20:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/fd/16/99f8df1d2213865ef208c18b833803c4ec532ebd8be78c2b814f11d43461/uuid_utils-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:afbbef5e19dce604ee4084b287d7633f4ae3f798391403106e976cc565014495", size = 290915, upload-time = "2026-10-19T11:20:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a6/ff7e16d04162d2e23dff1f13f4ce556407af2934ad0c13535b5a4ade58e9/uuid_utils-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cfa71f1026ec800206a517905666fe2acbc41dba4c8a6c51e03b17bd242541", size = 326927, upload-time = "2026-10-19T11:20:46.001Z" }, - { url = "https://files.pythonhosted.org/packages/3e/74/35a63f788b94704e8ceabdb3122de29a59b06e38a09484cc6725348c566b/uuid_utils-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36223ba9eea350ae41da2fe86f3b9a8399fd8420d02ee4cef4192356fd16c026", size = 333433, upload-time = "2026-10-19T11:20:47.711Z" }, - { url = "https://files.pythonhosted.org/packages/1e/fa/5ef4f20e556dc4a86a5212636cf1694ce37c2882e17b0f7fb2406592426f/uuid_utils-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe635129e607140e583099ba5da730a3cd8142a076660ac995a37b334dfab52", size = 439842, upload-time = "2026-10-19T11:20:49.493Z" }, - { url = "https://files.pythonhosted.org/packages/ad/47/be79efbae8e499bee2626fe4b2b7ebe17763b25dc894a79753dd3ce0a07c/uuid_utils-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdd40b58fc284badd5cc75620c61512e482957f884ba7824b5abb7081b871149", size = 327441, upload-time = "2026-10-19T11:20:51.157Z" }, - { url = "https://files.pythonhosted.org/packages/4d/80/ff80a4907e9cc12a8ae80679461f7e2717886f43fe6f1c8b054b7607aae0/uuid_utils-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815b81d226e5036cb8f3fbc44ac19ef2d8a454e557d6a9c1140b2173544c0514", size = 351434, upload-time = "2026-10-19T11:20:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/55/a5/140455d246fbf195f5c689a14ea6975f6a89262edcf28e0904094d3af0e1/uuid_utils-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b600f0de348beae5e7cded0de63cb3878725a2daa6be58b1309a81b4ec47be8", size = 503728, upload-time = "2026-10-19T11:20:54.686Z" }, - { url = "https://files.pythonhosted.org/packages/ae/41/eae8b1f19d4065a8acfcfb62ec3a1730c949f86914e00c202cf388fef8e6/uuid_utils-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:73aac44a18b21320869a58535adfeb614dfa4c5bd00deb7707918795eaace87b", size = 608130, upload-time = "2026-10-19T11:20:56.577Z" }, - { url = "https://files.pythonhosted.org/packages/05/2d/08bb9ee241dbf2d04164210653cb61025ca49d71fa5ffba88f04a5676c86/uuid_utils-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:48d225adeb56f8a5953c2cba5db1a0af3e65092c63d6d6bd458dea3be6a408b1", size = 567998, upload-time = "2026-10-19T11:20:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/93/52/2d57a327da74605218c8e76a2cc7e84019aa71361cd3f48d137fe002c03b/uuid_utils-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a7e5e9a7c4ef7120b83eabd2d97692498ef238694f3b78782fb90c8a9214f400", size = 532556, upload-time = "2026-10-19T11:21:00.253Z" }, - { url = "https://files.pythonhosted.org/packages/68/57/6c5d4fb35f36a18db78c279dfb357cc4881c252207ece15992aa4b3b2095/uuid_utils-1.1.0-cp311-cp311-win32.whl", hash = "sha256:08a5b7832a09d0e31438ca2adfe0eb1abc8388018c52e431d8ac113077cf19b9", size = 174300, upload-time = "2026-10-19T11:21:01.838Z" }, - { url = "https://files.pythonhosted.org/packages/e9/65/b1192ce7ac423f6be6533bb651b72120250036d60157fdd960fc4787df71/uuid_utils-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:115fa696d1c9bff28648c9588680b681af391ac9cf95789b3297625db47925bf", size = 181040, upload-time = "2026-10-19T11:21:03.362Z" }, - { url = "https://files.pythonhosted.org/packages/43/6b/fd57b669af524715a3b1c01caea4530dfcc6f8c032369f504fe9fdb68877/uuid_utils-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:28e7c3ae445753587c83b2128a67a0fb62b610df12c0495794097fd1bbd0ef88", size = 179516, upload-time = "2026-10-19T11:21:05.031Z" }, - { url = "https://files.pythonhosted.org/packages/16/71/310e4aeb7bf7d40524ecf5cabc1f6089c8f791bf013a8684b43437116d45/uuid_utils-1.1.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:949baf215402379b958d919740c11f5d07ec80a2a76275d51dcd9564c97f22a1", size = 559048, upload-time = "2026-10-19T11:21:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/4a/49/884e4da3eb23aa47d78af0e77d7f5535235023d415083e73615089d1f5dd/uuid_utils-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:af4ee2bb189a701c925ff952f76660340549e2bc1dcc33d4c30126f5d4aa5b98", size = 288928, upload-time = "2026-10-19T11:21:08.513Z" }, - { url = "https://files.pythonhosted.org/packages/51/26/1db0ed16af735beb1726be5de196583921fecbb35560f150d2138fb0deb2/uuid_utils-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64262199314b709a362668937a95ec060fb9adf463e8d784cbce1016ed1180dc", size = 322257, upload-time = "2026-10-19T11:21:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e8/5416eb6232de595ce41219738e58b84d5c832e4458eb69c1e0df69aa5506/uuid_utils-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:408a8dee1d4b60fa7529c88c6951c4b64653401450cd23d06703ef26c1dba820", size = 329266, upload-time = "2026-10-19T11:21:11.866Z" }, - { url = "https://files.pythonhosted.org/packages/8c/47/cf0d69c9ce5bf649bfcbe7f518b0d19905ad3aadacadcc0ce7097deba9d8/uuid_utils-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5095ba70c03b8d0957e1986c48d6e05536bb264812c7360577d996b6c416d81", size = 432891, upload-time = "2026-10-19T11:21:13.54Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ce/ae337d441c789041a916c1f9a8489121beb6f98ed7c505c537c79e0fea73/uuid_utils-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ef30b84f1f1729b59d076e9b270837b4716b7cecf07cce5a364ae2e0a01ceb7", size = 322893, upload-time = "2026-10-19T11:21:15.196Z" }, - { url = "https://files.pythonhosted.org/packages/75/66/61e58e51302a1a628e063701cb9ebf67b614ed2e4511c816cbcfebd361a4/uuid_utils-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:933a546d331005fdc96bf5246eba69079f0bafef438238e67094ee35a7522980", size = 344408, upload-time = "2026-10-19T11:21:17.197Z" }, - { url = "https://files.pythonhosted.org/packages/57/a6/fcb9e47d98ba7896feb11aa5ffa7257a6ca606589178494090d69130acd1/uuid_utils-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f661a1596ac5bf6e134a4179c1793c5e23f38909767ddb63fe5f072c43788ba", size = 500268, upload-time = "2026-10-19T11:21:19.156Z" }, - { url = "https://files.pythonhosted.org/packages/c2/0c/6f009764e6e44575d33869f7156822b18991d2a9b40de38ae7eec4d7288a/uuid_utils-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff56adad65ee9e100a00176b74cf97961217cae9df86f114f311741cc1edac90", size = 604776, upload-time = "2026-10-19T11:21:21.065Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c7/06d283872b36893b71c7d636a12ad1a2163e0d1d2aaea846d1d5488eb962/uuid_utils-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7220e61d39bf58e1f2a3a58e921109bb12857111ac533eaff5840998d4755773", size = 562133, upload-time = "2026-10-19T11:21:23.105Z" }, - { url = "https://files.pythonhosted.org/packages/2f/07/93016f7178524b5ab16a19e1114ab5f192ba984d2f2db8078061cf47a47d/uuid_utils-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f579d21cbd1e6fd7d40011919fe6243868d77c0381d72ca9b4f3b294703bb13", size = 527588, upload-time = "2026-10-19T11:21:24.751Z" }, - { url = "https://files.pythonhosted.org/packages/eb/76/a76ec9e1561e4a0265ae8f3292fa601263ae8519b273cdf8297d36552395/uuid_utils-1.1.0-cp312-cp312-win32.whl", hash = "sha256:9b0724915ce756ce137ff14be77b7f74bd8042112bb84f937288cad4eed47a86", size = 170780, upload-time = "2026-10-19T11:21:26.382Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4c/ef53c00298dc1f73c7722a4b550074020e8ee05fcbaf08f57c4d3263065a/uuid_utils-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:06d658535386df4f31769b13092e30d8cea03799a5874e9fa755847e299c1323", size = 176925, upload-time = "2026-10-19T11:21:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/9a/2e/9ba28f6b8fd0ff855890cf1a0e8ed8929e598428255682c9f72467565d3a/uuid_utils-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:f8db3f53d51c38b7ba4afeac10cdcdf4d0048ab6ea63d50a3c19d965c13f920f", size = 175402, upload-time = "2026-10-19T11:21:29.529Z" }, - { url = "https://files.pythonhosted.org/packages/de/78/5dbe559ec503ccc1e90f604543018a7a04047fb4b7e576f6b5b0a7f7d9c2/uuid_utils-1.1.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c63b56ba5fa87b3c9f83096ddf36536c270ba1021c65821ed0ba2972f10a6578", size = 562784, upload-time = "2026-10-19T11:21:31.252Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b8/d6161ff91777a2e22de3c40d81136a76ef1da8714dc9ad473a9df204b20f/uuid_utils-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdea92a41bf3acd3b11a95585bb0f80fc798d8b48c89771e6be535bc268eeeac", size = 290517, upload-time = "2026-10-19T11:21:32.845Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/7ac2979c06e0092e94ea2ccc37c7298628b7e2998310e53e12a4a36b9c23/uuid_utils-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de74bb1d45eab119dfd62bd4383fc5f0b9a5cd652ac1655d66e4b87d2d53184", size = 322563, upload-time = "2026-10-19T11:21:34.723Z" }, - { url = "https://files.pythonhosted.org/packages/8c/5a/5bf030a351764832a7b794895ba5b8d5edeed93635de9f945d1e59654f0b/uuid_utils-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e5d6ca1133ec922498a423e1fa2fe1b0899b2294761250aeab098dcb6219fba", size = 329111, upload-time = "2026-10-19T11:21:36.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/2e5f293c9238715285b6a20265bb6fe24bc85dbf30b843464cca20b07d91/uuid_utils-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54a2a6b0c377202610415c6332733d517caf2d372a0fb26de28400d70e99febc", size = 434137, upload-time = "2026-10-19T11:21:38.145Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8b/c757327f35e9ed9d8113753465b1a920ec0df384a5e4d8c7dbb2246d1d07/uuid_utils-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40cf778e0b2bcdc3aa6235f18e883315a12c47b64c77a8006140571cd2a7bd59", size = 322680, upload-time = "2026-10-19T11:21:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e0/d84c15acf4dc4e8e0f2e822ed1fa20f780de020eb9b1023424144b6267fb/uuid_utils-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b524865d84c36e2705c78e295210ed2cc8d9030ed85fc723e81526294a331c1", size = 344102, upload-time = "2026-10-19T11:21:41.582Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3f/bff84d94a0bd4119a1eb3948459cf48d8c659c91b582411c755091a8edc2/uuid_utils-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6554d6ee237ff2f96de146bfa1d3f2603542683efe7c3ec013b2b00ce9952756", size = 500707, upload-time = "2026-10-19T11:21:43.452Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0c/2d901d58d8e1db1e898c033b88e310fbace3575b4d0ac18b3a20cd4a7776/uuid_utils-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f397ccdff523b700238a539249837ca9773195d4f64a76a7ca77da25eafc18fe", size = 604743, upload-time = "2026-10-19T11:21:45.225Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d9/95e74faddc26f3862b191b54957e76aaa9ba9503c912648ca83cdc04dfde/uuid_utils-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e2cadb57502461ec33a667888b6bcfa30ef34f522f182c752e74ffee781ddcc", size = 561361, upload-time = "2026-10-19T11:21:46.963Z" }, - { url = "https://files.pythonhosted.org/packages/f1/5d/a7332f64b5cc63f993cbeeb0cf2aa02bf0722b2c038312563cd0a548216b/uuid_utils-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70b5e208a339ad8a01231b2531846e598070efb2a126b0529191a084a4dfcd0e", size = 527665, upload-time = "2026-10-19T11:21:48.562Z" }, - { url = "https://files.pythonhosted.org/packages/10/b8/6ce880dd3ddf3a575d56e77e9337adc36117ea1ada850b39c0d50e682ff4/uuid_utils-1.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:001cfb0b3183164a23b3a551101ad0f22981c44f8486e63220c512e44b283fda", size = 99632, upload-time = "2026-10-19T11:21:50.113Z" }, - { url = "https://files.pythonhosted.org/packages/74/61/60d34c78594cbaf6b0459b9411bfd586540c640e66ee75ddd0cb2d2f0074/uuid_utils-1.1.0-cp313-cp313-win32.whl", hash = "sha256:c9725e8e371472c03d7fb7669abf9d855c1a6d3347f1a425e55434c946aae94a", size = 171266, upload-time = "2026-10-19T11:21:51.638Z" }, - { url = "https://files.pythonhosted.org/packages/c4/34/d88702d2d2173dac1d4b39c85dd8dbb990321578aa4da96b31c362b7dea7/uuid_utils-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb3047b35012d8623814605411cf86f4d08de3043d0405dd8ac64d139539335c", size = 176665, upload-time = "2026-10-19T11:21:53.486Z" }, - { url = "https://files.pythonhosted.org/packages/87/75/1e8444fc4d62b64c4c425f885693ec4f42a497c8fb026be965ccbbeb04f9/uuid_utils-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea12a2ba99f9bb9f3e761b79ade863635c8f467c6e6f40ae24269c5dbe2bad2a", size = 175333, upload-time = "2026-10-19T11:21:55.555Z" }, - { url = "https://files.pythonhosted.org/packages/81/e5/53401292e8850ba7b3b5ef3fcc7de6a61ca34e804898cae00961af16ba8d/uuid_utils-1.1.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d8e8f28ca5a80563d9ac681098416718633c80a1f1ed955c4d30e18e4a9e8075", size = 566662, upload-time = "2026-10-19T11:21:57.483Z" }, - { url = "https://files.pythonhosted.org/packages/5b/2a/bd1f292edd4175a4c2f2c6508e687ca5a057d2fd23b79ea3fe070a56938a/uuid_utils-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e2c3f3c3e969b2f900543e27514c354eb145ae8ca0af1fda4ce4d006746fcb6", size = 292298, upload-time = "2026-10-19T11:21:59.313Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/92c25a0c7d756cabcc6f90aa34da84d7dfae801be9c2b15bf7b8a35f54aa/uuid_utils-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b366c721af0fdfd08bd9ec8242a97d07d8e3569262bab5d6152ec222623b43c6", size = 324825, upload-time = "2026-10-19T11:22:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/51/b0/80e5401c7fb444b0e5bacc68226cee86d09c22e09fb11156e99dbfb73ef9/uuid_utils-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c465dcc77dd101053251b7f77fcbb31c6468135e93d478d521e745b441d8da8", size = 330975, upload-time = "2026-10-19T11:22:02.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/87/a96615d5d80740cba5e91d730bad37ccee027aed8ace5b773131e5183544/uuid_utils-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:036d204dc1aa20a4a1c10305c4b6da5b8f56bf98b56997409c0c92745213ce83", size = 436353, upload-time = "2026-10-19T11:22:04.437Z" }, - { url = "https://files.pythonhosted.org/packages/f0/62/ceb21bbeb93af570e0ae169f0373295062726f81a8c4a6ef953370784d7c/uuid_utils-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698f2f078dad63375b26badd3c792d0f7b81f515b99091018f31166a0d14cab7", size = 325424, upload-time = "2026-10-19T11:22:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/90/99/81fee7a0e9758b87daf8727ae173c6c51d110d4d70364cc1ae003d2c85c6/uuid_utils-1.1.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2151a82e4374ea2b88e8b173e20b676a62abb013a807f832758b2bcc9997874", size = 346584, upload-time = "2026-10-19T11:22:07.769Z" }, - { url = "https://files.pythonhosted.org/packages/90/10/fbe1ae3911220adaca2a7aa1e7a308b5d048cf634fff4c65f42b56a1dc23/uuid_utils-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb085fdfdbab532351e45a4957387652407dab1b145be9be00836c09be6fc3ec", size = 502761, upload-time = "2026-10-19T11:22:09.65Z" }, - { url = "https://files.pythonhosted.org/packages/8c/22/4d9da0e3e8a94d7c70b15dea17eb1c7377a3e3390f91059efae22ba83551/uuid_utils-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d018cd1e3e71e215fb5a52a1dbe1d01f315c081158abfd92301611a091ec55d5", size = 606432, upload-time = "2026-10-19T11:22:11.367Z" }, - { url = "https://files.pythonhosted.org/packages/ea/98/8b955bd7f984146dd332072c3869274f386de4a760bcc72e104d020aa941/uuid_utils-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:487ce24cc7455b8f1e9080e9cf0699fd133f08d169ddbacd73e42f6f483e5663", size = 564644, upload-time = "2026-10-19T11:22:12.987Z" }, - { url = "https://files.pythonhosted.org/packages/36/5e/94fca160ca6b80c6b3de63d3056c76add0bb852519519e5e96225faa823e/uuid_utils-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bcff122d2781ff7dceaa1a79ee9b4ca4ba9dec3fcb3c1bc1a176da8a52789227", size = 530534, upload-time = "2026-10-19T11:22:14.549Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ac/93f18f4d9de9eccff8042fe1e054002b8d3307ee80ff6337e85076b8aaec/uuid_utils-1.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:da28d6a0c676f1fa18dbb05bc521ba3a8b5843851fa3f4a07e6fe01bdfad2ff8", size = 100551, upload-time = "2026-10-19T11:22:16.152Z" }, - { url = "https://files.pythonhosted.org/packages/33/7b/f48a6bb58528a8f24d54c92e62f01a08781c522490339631e2eb7f37b624/uuid_utils-1.1.0-cp314-cp314-win32.whl", hash = "sha256:0512c36dbd169ece6f2bb99b90c23a87983e3b477bb2ae95b6ddb8517d59b8e2", size = 174077, upload-time = "2026-10-19T11:22:18.15Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1a/63025c72422a26a7f858357cc15757390f546f2634e2a2a98eccc995e4c2/uuid_utils-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:5ea4c693b2a6c27aa9c53216d5fdaf159ad667e56028e3f5c8e64b64944901ad", size = 179753, upload-time = "2026-10-19T11:22:19.91Z" }, - { url = "https://files.pythonhosted.org/packages/53/b4/7b20acceaae588cd13af524f0a60244f461e0fe6161910b13df141e4dcbb/uuid_utils-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:607a35b2b6dbd9109bca42af3d52378f812cb014d8c0c9ec40d32eeb5cbf656c", size = 177607, upload-time = "2026-10-19T11:22:21.428Z" }, - { url = "https://files.pythonhosted.org/packages/d9/21/11459ab6cdd1b0e8c3f1dead90b4ba036d67c69fe74b5de65356e49456d8/uuid_utils-1.1.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e7b534b72bedb3efe03d2f71631c4e1028952398380c83e75c3f372a3b3d43a", size = 567221, upload-time = "2026-10-19T11:22:23.079Z" }, - { url = "https://files.pythonhosted.org/packages/9b/29/04ba922c99a1fc823c800bb7e7e81261982b7075b8af0314b8c114d66e73/uuid_utils-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:162ee2e2ec3f1ad43fb071377fa9d304b7968775072f5766e0992832a8babba5", size = 292732, upload-time = "2026-10-19T11:22:25.014Z" }, - { url = "https://files.pythonhosted.org/packages/be/a9/0ec005f2c53d14e299bb638ba7e1d1c85c2a98d578d4db32c85c06ada869/uuid_utils-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b65aaf9c39935a4e609d00ea77604c93bb32ed0dc7255966f8cb5b1bbd0591d", size = 324460, upload-time = "2026-10-19T11:22:26.895Z" }, - { url = "https://files.pythonhosted.org/packages/fd/75/1aa9c440d4f1f7b7e38624d8abd59f7dc3ad75bad8980765255f23919c83/uuid_utils-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:358a7de0ff8f47bf0552d6ae780f95854c3b048ae00ecb3bbcff3da93efb4041", size = 329524, upload-time = "2026-10-19T11:22:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/3dc711f731481ac66f7b597e4b30b3dc84de238032760d6d70e26078038e/uuid_utils-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9316d4d009b3881388bf53f026aadb890c459c259f107e945067ca3f3100698", size = 436207, upload-time = "2026-10-19T11:22:30.58Z" }, - { url = "https://files.pythonhosted.org/packages/19/8a/6cab173e1f6c43a0ad09232968d2185fc0298e1d656336dc23558ea93ee0/uuid_utils-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:402c619b7e934c48c8950bb8542808e9b0328da8f427c08295efe949cf497bd2", size = 326382, upload-time = "2026-10-19T11:22:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7b/16950cf22fbc47fe045cec6b43d7ecd21063775304920123d92f76f64976/uuid_utils-1.1.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:094d4e4b8f64f1aa5e22e59ad10432ddcf63e01d2ef3ff1489b878feaefd748e", size = 347139, upload-time = "2026-10-19T11:22:34.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/3a/6baedc1aa6c0446d8c0a8961aed7e3e704757c1c4865afd91e996b1de80d/uuid_utils-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:28d36962be414c3543c1dc39a76caceb0f697856d0f83fe149789688218a5437", size = 502078, upload-time = "2026-10-19T11:22:36.293Z" }, - { url = "https://files.pythonhosted.org/packages/71/3b/ad3ef3c100c79a07655d919368f86afc8e627be43564267b6bfd91f3ff29/uuid_utils-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f836bbaee4e704b780dd64f874386ecefc1083b5b608e92128afa2a6b236e376", size = 606179, upload-time = "2026-10-19T11:22:38.358Z" }, - { url = "https://files.pythonhosted.org/packages/d3/39/20b279df56f9f24937be18abf9cd2bba13bca932590eff7dac55d4c9040e/uuid_utils-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8ef69a74258b265894e5bf9bf2bbab488fdee6e053fbe863bd084f062ff007c0", size = 564949, upload-time = "2026-10-19T11:22:40.28Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9c/4d1ba32ee94caddaba0077660e781734ffbc80084511fd8d542ca918a7b2/uuid_utils-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a64838ceea2b32154703235788cecb2728815b17eda61843bb2a136f0ee3fd16", size = 531382, upload-time = "2026-10-19T11:22:41.899Z" }, - { url = "https://files.pythonhosted.org/packages/c4/eb/3c5ee173a1c3f50a88be996bb36f2241f6a17e100cd27c3c0767bb6a7d5c/uuid_utils-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:e8530849a955861eeda845851dffdb6cef9146aafa67a484ba8b3890e337ef08", size = 173614, upload-time = "2026-10-19T11:22:43.63Z" }, - { url = "https://files.pythonhosted.org/packages/42/d7/617d2b6042d242e52dd0d7b05cedd2ee28f7d17bda12846951381021d961/uuid_utils-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:deb78d38ddc98d99dbd269cd3379fb891300e4034d47dce8999f62742f0e229d", size = 179325, upload-time = "2026-10-19T11:22:45.289Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4f/3498f81f4071019b25f930f1c3ae60112adf5e0facccd69043c7b50b882e/uuid_utils-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cba5571c253bf2eb0c9711616957ce0c500e31cdcca4f4323d5ffc2792eb092f", size = 177344, upload-time = "2026-10-19T11:22:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/88/c8/680ce527c486ce94c257ec9896acd23b9fbb6599b53de828ba43c815e63d/uuid_utils-1.1.0-cp315-cp315-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:71595f8cc7b16ffc909ceb97d552e63d7f3c968cc43029c548d7c9c4a883f154", size = 567374, upload-time = "2026-10-19T11:22:48.822Z" }, - { url = "https://files.pythonhosted.org/packages/56/c1/393c5f63869c6f16a707fcf5dafc5185848f69efea84591897f40a7579a2/uuid_utils-1.1.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:2ea7484d9c224d00f331caa17babf3d3e5baebfdc32317b4911411d03c2f7de9", size = 292869, upload-time = "2026-10-19T11:22:50.397Z" }, - { url = "https://files.pythonhosted.org/packages/13/b3/1f9f666729309f773d19226d12cfecc07fa512765489ba69b91a46375810/uuid_utils-1.1.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a2ecd639e80b03018fbc37857a007ffdfe1d2c82c3628706d84b6047b80f781", size = 325049, upload-time = "2026-10-19T11:22:52.463Z" }, - { url = "https://files.pythonhosted.org/packages/5f/06/18f8e143d5c805be1c1f85753edf24bf871c7dc7359df17ae26f9ac25eb1/uuid_utils-1.1.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7efcd15d845a7d9b7d1f40b094094e9f0378d4d6f130bd1ab6151597c4a3684", size = 331997, upload-time = "2026-10-19T11:22:54.293Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ce/4e14563231d40e81fe88424ee09f7903435e3e0d5bb81616c9a34e7575a5/uuid_utils-1.1.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06e94f1f8d33681c0240b9ed971e8393432f02f6e5254c5d8e1821ed1bafc748", size = 437362, upload-time = "2026-10-19T11:22:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/88/7a/3182b8f231baaedf64240e6b0b18712ce9036ccf5e4a9104b9ab8b928d09/uuid_utils-1.1.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:963b812f0b06291c97e20a14b14ffdb519644fb9856e0a299e8722fba8fb099a", size = 325630, upload-time = "2026-10-19T11:22:57.698Z" }, - { url = "https://files.pythonhosted.org/packages/d1/39/bb3b5b72939894a292d34b1ec499e0390ca16738a8d03a60f7bbec851f99/uuid_utils-1.1.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d73586cbe8a70b16141a92a3fe81102ffe9e5c0b8970a8be7720526d72098d5", size = 348301, upload-time = "2026-10-19T11:22:59.507Z" }, - { url = "https://files.pythonhosted.org/packages/8f/5f/99d9c8f066aad104f0a157d5048b2e469ad25539d3a60b3b362b54fc1b10/uuid_utils-1.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24c399612223410ec1b4a924da3011f4c8aeb333cadf44762258f687779448b4", size = 502848, upload-time = "2026-10-19T11:23:01.699Z" }, - { url = "https://files.pythonhosted.org/packages/7c/59/0a771b2248503807cb0d34d49e0a2f5d37570fb0c0a4f7b659f7261453a1/uuid_utils-1.1.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:1c46bf2fc2b84a7e1b305191a42671f7827fcc0d6b864939dc03136c6f095fe9", size = 607067, upload-time = "2026-10-19T11:23:03.39Z" }, - { url = "https://files.pythonhosted.org/packages/bd/68/3dcf95eac42adf9012c3f8d48de2bf3ea1dc4b07961bb728b280514c2713/uuid_utils-1.1.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8628fe3da953923e9a0d932a9b6cfed8c96e6b5586ca3dc87d9d3ab685ee7bf2", size = 565855, upload-time = "2026-10-19T11:23:05.508Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f5/5a0c60e658cd59d9fd20bb396c7990b0750c3e8914a3160ce79baf0a7a4a/uuid_utils-1.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c295111c8be5cf85fa06fcfa1e145a8242470a6852b38560780e133f0a842f09", size = 530734, upload-time = "2026-10-19T11:23:06.991Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ae/fea64495f955bb624884404454ceb99d9cc134696c06dbeab6ed011711c5/uuid_utils-1.1.0-cp315-cp315-win32.whl", hash = "sha256:ddc784b3b902df461ede85ce52ea825e7401d1ab5cd2efe34f0a81e5c7664a04", size = 174553, upload-time = "2026-10-19T11:23:08.502Z" }, - { url = "https://files.pythonhosted.org/packages/7b/14/8478bf1009f3aeeeb86461296fb6cf999bacb3d937b7ecf2e65d38bd07a7/uuid_utils-1.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:065acf8b353f5eb94170f63fb11f08b0e4547cdee2d5d2a04586731cd88c1d73", size = 179363, upload-time = "2026-10-19T11:23:10.082Z" }, - { url = "https://files.pythonhosted.org/packages/0b/84/5de94ef688abfe1943a81be0c5180221c8723d798d8e09abf943b6dd768c/uuid_utils-1.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:a864590871b42e32e8729f315e324faeb440381ba34efd9f1ffbab01ee9ef325", size = 177424, upload-time = "2026-10-19T11:23:11.675Z" }, - { url = "https://files.pythonhosted.org/packages/99/e2/768633dac9c3774de5ad9e6ab401fe2a2435cdeff26102f4680054027f2f/uuid_utils-1.1.0-cp315-cp315t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:55c0098390cba6e5bbd9cc01ba15d56027420dcf8a5b63ca0f4dd85c2c286aab", size = 568306, upload-time = "2026-10-19T11:23:13.342Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3f/4aca8f432f7bb823df10a451c1b8bd4ddb9be7ee149f5e6be1be0b869c23/uuid_utils-1.1.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:d7d9f00b244434332e9fecba615e221277a46eaeef932abf4f784ad6595e25b6", size = 293017, upload-time = "2026-10-19T11:23:15.054Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c7/64539a51b50b0c91dcfe556a9f5552891be86cf9dec1c1bf791f94985168/uuid_utils-1.1.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6727381e7d22e44ef749e2ce3cfb1ef6c6b2934556a5ec09f3b4b86b14752644", size = 324337, upload-time = "2026-10-19T11:23:16.608Z" }, - { url = "https://files.pythonhosted.org/packages/94/4a/1910e168bdcef00a4a2b5542e03fa37d535c8b14f5c729fcac9317fffbc2/uuid_utils-1.1.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75416b7e93e665524fafe0cdd26ce3aa7a1d82fcc1eebcab16d75de507ae28c0", size = 330108, upload-time = "2026-10-19T11:23:18.374Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ab/9d4bfb49b11095eb004b275cccf52cdf8232a6d380f3d21a51ac452b2eb5/uuid_utils-1.1.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f744a6b616cd1c84b7dfcea234f772a61801d95e0e7b30006e4488df1faca", size = 435646, upload-time = "2026-10-19T11:23:20.132Z" }, - { url = "https://files.pythonhosted.org/packages/57/75/7a1aeb280eee7e796d4823228a429f9cae52dd9b54510ccac8919c973a21/uuid_utils-1.1.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13de6fdb1aa02b0c1a2959292c2bf97afca3ead0412bcbc239ea196b0f03877c", size = 326692, upload-time = "2026-10-19T11:23:21.748Z" }, - { url = "https://files.pythonhosted.org/packages/73/24/f2ec38c6446719e03964c9559f30ecee919f4adc9ea6f7ec351ab5589f14/uuid_utils-1.1.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d6d4c4e11597b6af7bbefd26e7574765f6ba488df946ef38bf2c6c8cfdd9d045", size = 347615, upload-time = "2026-10-19T11:23:23.701Z" }, - { url = "https://files.pythonhosted.org/packages/95/e3/8bbb8105d70d73f0a4f8d4a02135a20be911e5ae7268ff618bf0d271233e/uuid_utils-1.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3faa2e0852da89962f20e8a4cee49777fd83b651095ff10f87a2349c7d94e655", size = 502377, upload-time = "2026-10-19T11:23:25.513Z" }, - { url = "https://files.pythonhosted.org/packages/7e/41/79e1ccb5d25cddf17f38c6bca2a437f336abfd7b165b21c334dd6148b916/uuid_utils-1.1.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:8bc9c328bf78128bcd3fa040394c4b986c4ef13baffef0ca75fc66a5d44d8628", size = 606900, upload-time = "2026-10-19T11:23:27.168Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1b/4bfebece1cf1bdbbd3600f8e6dc84a26a7ec27457db82366b2450090816a/uuid_utils-1.1.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:3882e0bc4d82655bfe504918a9d4e0aa780122350653b6d6edbebac327a51208", size = 565320, upload-time = "2026-10-19T11:23:29.263Z" }, - { url = "https://files.pythonhosted.org/packages/34/ef/2eb1adab4456145a1297eaea41f8706d33d922f0f93d1e8070c1a18ecb8a/uuid_utils-1.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fd42a5ab5ccdaa58ea0ba74c36af2d2ad9df9faf9bd0cffda9c6c453acf1c4a2", size = 531494, upload-time = "2026-10-19T11:23:30.966Z" }, - { url = "https://files.pythonhosted.org/packages/f9/aa/bf056eb703c4ed8d73b4053cf031c54c09b13af73846f3c591b2f29f8358/uuid_utils-1.1.0-cp315-cp315t-win32.whl", hash = "sha256:1b02672c94234bbbfac40aa6bab593f0a14b2eaa7d0f720420270d001bad8777", size = 174031, upload-time = "2026-10-19T11:23:33.075Z" }, - { url = "https://files.pythonhosted.org/packages/24/c8/941467babae8e58e765b63669c9d79519151ce2be22d95d244d352e66fe9/uuid_utils-1.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:40185e2a4a033c4c17c33c93701ad79b2c0d8653b7dc7d5d7b3a0006e78dd7fa", size = 179531, upload-time = "2026-10-19T11:23:34.521Z" }, - { url = "https://files.pythonhosted.org/packages/07/bd/b1057910536d9b80fa9fe5ce9327acb5b01fd896e2ce51b132e5cf76fbb0/uuid_utils-1.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:85059434bebb24c460bea1fd5bae481439465ba94b09a770cd3c265409de7f2d", size = 177281, upload-time = "2026-10-19T11:23:36.427Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b9/18ba310a5b330e100e57ad290a3ecf8849eff37fbd14be428cf9746abbd2/uuid_utils-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5cc7bce5b972074b707515747d27d681f64a1fb571f50bdbc710a9b1ea3e9479", size = 571793, upload-time = "2026-10-19T11:23:38.176Z" }, - { url = "https://files.pythonhosted.org/packages/59/57/f5f1357c91c9da4e589989f34be49f26c76fd04bc270a059d609d15e0490/uuid_utils-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4159199b6a7879ad13843acd05b636c4c390bf8df7946ee567481c6693321b6", size = 292091, upload-time = "2026-10-19T11:23:39.635Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2f/7b7824c7cf9127b47bd72325f35b5ef8841dac37880001d9dea3f9a00b17/uuid_utils-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea48336100e8c91309b4d8ca584ba88aca846a17f2799466a28160a5b069601a", size = 328475, upload-time = "2026-10-19T11:23:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/9be0651439525d8ca77718b30e7216524c282432c08e1421db7dbbd56209/uuid_utils-1.1.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec2eec518cc0c45d42756f14072a0eab6323f1f6d1ee57626a43ce071e180415", size = 335624, upload-time = "2026-10-19T11:23:43.152Z" }, - { url = "https://files.pythonhosted.org/packages/83/66/433d85f1c7f20e388bce690eadbec4a543bde09042fb38bebf4e14634b1b/uuid_utils-1.1.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67fd31785e9b012844500b77b7bd7054a96e9bbfa6b7039f323653d83890a414", size = 439653, upload-time = "2026-10-19T11:23:44.918Z" }, - { url = "https://files.pythonhosted.org/packages/df/6e/f4af18dbaf8a9b689e17788f8b06b0152b4a85e135658f286edc7309974a/uuid_utils-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a311dfb8d870dabc7d108b91c187b2701537133e0151a1c112570d4eb2a187b", size = 328513, upload-time = "2026-10-19T11:23:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/09/78/8c7e51c5c08e50ba60f97e002dfb6272866ef15efd8659defea30a4938ee/uuid_utils-1.1.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97cf6a4ba2b55779efae448b01e01ab7af182ee090fa82ea09a17721cdd01701", size = 353620, upload-time = "2026-10-19T11:23:48.405Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1b/45862dd20ac4ae3547f07ad395484cfc816cd39c345fda7079a4da9aa45a/uuid_utils-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:43690470a0f0ebe30a369aa32093ce9092397088cf3e177ac4112c6b95d5debf", size = 182168, upload-time = "2026-10-19T11:23:50.009Z" }, +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/35/2e9666504bcb3ab50656b86cd468880cedb86d3c97b88d3805dcc124b95d/uuid_utils-1.0.0.tar.gz", hash = "sha256:8ed2e0156d29c4cfa0f931b4b71b35d2705d84054f63ba07a78f7acc2eb09a5c", size = 43759, upload-time = "2026-09-08T13:27:28.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0b/9dd7618399c34481b4bad422d8c15c69a659bba8b1f1c85aa3198cb0cfbc/uuid_utils-1.0.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:55b8912f743b0026aa72a5ff392831610a240fb48eeeccf945db60f74645cd2f", size = 564415, upload-time = "2026-09-08T13:25:24.193Z" }, + { url = "https://files.pythonhosted.org/packages/03/00/6cb04489068dac102ba11e089fdaba32caaae79edc94ba02c34b7b460278/uuid_utils-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2ca478b10c68b57e47f22c5baf4ffe7c4ac922db776573840efbd4b34dfe4cfe", size = 287836, upload-time = "2026-09-08T13:25:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/12/f1/01c95c433a52a0af7b03d1e54f3a5546f22c2e0bee769e3fda4aaa7c11e4/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66cb850461fd4786da6452a8ee1a7efe882de1ebe395ddfb18f27b25ad96646", size = 326987, upload-time = "2026-09-08T13:25:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/a1d23ee04a10631b2ffcd014aeb57d29bcac35347218f6e07f5f4f7890ea/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa81372e1e52bcfb6c68bd77f194a87fec3aa254a5c87034aac71dcd232dd5e4", size = 335823, upload-time = "2026-09-08T13:25:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/91/61/ea0d8b5d04fa5f9c8f54e637aa4576522f1fbda62e1020352c9906fb84f4/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bbc1ea51dfe0cd70c1d1aeb493c70f7f55d3d82eb04fa118c0245715f22a312", size = 451699, upload-time = "2026-09-08T13:25:30.476Z" }, + { url = "https://files.pythonhosted.org/packages/63/ea/e88474c99b4b3b23f8dbfdb9001e649fe7ca933453cc5771b52583e0705d/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4857774b37bace3f02059e434642bd30915d833427bed44e9ee061e567e69ec", size = 328498, upload-time = "2026-09-08T13:25:32.14Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9d/63469d3e5c22ea141fdbab03893960c6c9efa35191198729c06047bdaf73/uuid_utils-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d379ee954edc03653b89b4e3bfa0e1075a5d93220fc6f75e4a449c61467943eb", size = 353647, upload-time = "2026-09-08T13:25:33.513Z" }, + { url = "https://files.pythonhosted.org/packages/48/98/522a446c61887366da0602b17316598d85c58fb328ff9ea803f2cdff7ca8/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:85152c854e28f9ae66c204ccd003ebad64529c0e3d69f4abd4abb4da81fa6f4a", size = 504592, upload-time = "2026-09-08T13:25:34.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/f3163e1191596a79a575b102bf0a268a5a085f2478e06abdf7d1e89b4aa4/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fcbca750bc45a2447c077d19b77eaa2b9dc89f80c3552cdd0de8d0d0a6194e06", size = 610920, upload-time = "2026-09-08T13:25:36.719Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/2311bc6fc1f29cd2d25285584a3d10a90e2118b318d8a439f678c0a17da0/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bbe412cb4d2d52e0fa7d6bcaffe7985f06f1e97021e180b77a721dcbdb44ca00", size = 569925, upload-time = "2026-09-08T13:25:38.095Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/aa4836257ca94bb31e66fda030e645a35e64ad9737e43b6c30d6d0b2fef9/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1aadb47e436f6624f3d5eccffdb0806dc820df724dbf4d810d0cb3fa9d3e888b", size = 533882, upload-time = "2026-09-08T13:25:39.523Z" }, + { url = "https://files.pythonhosted.org/packages/29/69/7e45f40c3441fac3fa97c592580db51ddeb40cd4e01772c8fb6f6f0da152/uuid_utils-1.0.0-cp311-cp311-win32.whl", hash = "sha256:20d82f23c2879140b5b6338c4e2f8f9e56c8af7dcf5aab24a90c8c3629bc3d67", size = 173355, upload-time = "2026-09-08T13:25:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/82/97/7b6038fae08834c66a083549e41905fc5376589c9d9bfeb28c3a6f678d5e/uuid_utils-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:91c2fec6dc8633a1e15f4774e6ba36b3584c15c9b66298dcc4628c2c1009fd94", size = 179819, upload-time = "2026-09-08T13:25:42.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/cc6fba9782410132d8352ef0da0f5d09572589d4a74b5a4c8108b3da5b88/uuid_utils-1.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:cf29f16b2675a11429576c72922bb9008a5f46b16ad5dc42415c91142fb4554a", size = 178297, upload-time = "2026-09-08T13:25:43.78Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/57ecfa3d19021dd361d54c3213fa504122e24fcf379a6804c9077b2ec8cd/uuid_utils-1.0.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:20d6b4ea345912ecf2ba0dfc0bee0714c40b092830f814f1b768c33a55a38da0", size = 557800, upload-time = "2026-09-08T13:25:45.277Z" }, + { url = "https://files.pythonhosted.org/packages/78/31/fc8cab83464720c384082398f96c25b2b77de327485d436cfc73aba21358/uuid_utils-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:75f132bd55a715d091e8b2e91a118862a203777d34c4b2794caa218de0cd947b", size = 287237, upload-time = "2026-09-08T13:25:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2f/496b126dd703e12b33637246793abd97e05fa163e764ada0be4abca37056/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ad43743b1b6dd791798c37a5163d236d57705fa32944cee35c5d4f06c23009", size = 322768, upload-time = "2026-09-08T13:25:48.196Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/627a187b22b97b29aa7f3af02edd898fcb33c472c8c4898c6f5103fb868e/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:129d2e1245c0f54282cdfcfb9498342808fdf9259782aa01e09d15e4c51b8a83", size = 330985, upload-time = "2026-09-08T13:25:49.778Z" }, + { url = "https://files.pythonhosted.org/packages/f8/20/5bf65a065f369ce0fd8a031688c2767265ae3ee0e6293002633e2fd1dbdf/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64afb1db6f732b9526cda719275922ab9e3a4ff7dd255b89f40709e65c70dbb2", size = 445612, upload-time = "2026-09-08T13:25:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d6/d3dc9b10ac5d6453d5225459b5bc2a2a7a9f53b7139d13727aa64661c2da/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95b5ec6b070e5e3f3e02f7d195a22b90c1037afbc41006f122fb6d0499938276", size = 324362, upload-time = "2026-09-08T13:25:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/11/0c/aba31a49583a59dd9022136a0032ffc6a8bda71d7f894da289aaa63f48d6/uuid_utils-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f401251b95ddb077daed0871d4f43b4a7af88c80da595329206e734b4629e7d", size = 347120, upload-time = "2026-09-08T13:25:54.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/ebefd7241f763ca0a37ec7caad67e2d84e311335f74c5f3cf6ec52ce2e2f/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d8b55e9759506f5b5849747f6fef927d3d81970d8c20285a99e797119ae3ca5", size = 501346, upload-time = "2026-09-08T13:25:56.135Z" }, + { url = "https://files.pythonhosted.org/packages/47/0e/7d155c4ea6af24eab30ff926d737d028f4cb356f6446f23efc9cbefeb44d/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:2f9b1f16576237171e2782390f95d888dd7adce851fc85016e4b7b25d1c89fc0", size = 607597, upload-time = "2026-09-08T13:25:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/88/79/87708b9b618a29883d6b7be62569972aa5f7bc4af63338bc496532b5640b/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2ee3fbcc187d2b46e1bbac64203fca75b24451f1d39a436b8196d1445bf79014", size = 564136, upload-time = "2026-09-08T13:25:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/0f53c4954944311d51fd6ae9db25ad705e6c433d0db04c753b0743aa2c2b/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:083b21bba1adef508f84f8ef8da3fe2f085bd95988b1c2333618f06a804efd16", size = 529072, upload-time = "2026-09-08T13:26:00.857Z" }, + { url = "https://files.pythonhosted.org/packages/be/5b/4124fc1794ce8ed5fe4ced71ececdc0cacfb1d08c6d192c735402ae49d58/uuid_utils-1.0.0-cp312-cp312-win32.whl", hash = "sha256:e8b27a32095b43eb9e4abcc297afc4d4f4b130e9fcf9c9d09f93eec1382d1f8c", size = 170398, upload-time = "2026-09-08T13:26:02.437Z" }, + { url = "https://files.pythonhosted.org/packages/83/23/f1eacc16c91cd78ff86e62990b650adebf65782c2b992564c41311201662/uuid_utils-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac9e2ed981262301c85e27520b2564d03102bb8be95130e3085f0060add619be", size = 175625, upload-time = "2026-09-08T13:26:03.771Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9a/729645992d308d2806043cf5e8add10413b072574e0a25b3fc014053cc5a/uuid_utils-1.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:6988755b05dd27af81da59ae1bf15b14226e824d87dd98b60559d116df6826d1", size = 174128, upload-time = "2026-09-08T13:26:05.089Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/950f27905400b098797d8996d914fbc7faf73e4eb7be2ed5b5bbc16005eb/uuid_utils-1.0.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f82137ecd4ffe69134ed632ca865587dd44ddc5caf512cfefe181c0f6eba4cb7", size = 557666, upload-time = "2026-09-08T13:26:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/da/b5/aae34a85fd138c084440a0cc510cb245c11e5696f79e54f1a36225aae3b4/uuid_utils-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9ab40d7cfbbae6b2f291e597a664d82b8aee38debec9dac83c951adcf2c6c331", size = 287174, upload-time = "2026-09-08T13:26:07.968Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/0df3e19cb56969514d14b466069bb6c14f10f8d711f1b67487f280f34c6a/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1c12ee0c50756a35047fcaa7faba50b464462e2eaae65d60b608288a246191e", size = 322917, upload-time = "2026-09-08T13:26:09.442Z" }, + { url = "https://files.pythonhosted.org/packages/14/77/07b9c92a711c69c0fb6bc25b9b7f9de2fce00dde7d56e433b5475cceed1a/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab2b2b41690c6207ffafb01d0bc32ef7b3da56342b2b861ff5e03a772e39a928", size = 331363, upload-time = "2026-09-08T13:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/f28c80de9657aeb2fcd42ac0e5409f9dc22b3dd6438708e083bcf41c3a1d/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5f9fe3dfd076adc4460d3ede28c9225d47f998f77af77e67cb9d1c1ca935a0", size = 443183, upload-time = "2026-09-08T13:26:12.432Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/49ad8656c0c0565e17caacf3cf0d270605ca98775a957909d60daf7754d3/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:184b79b46e85c322b537b48d81b750952f6874db8e0662441b176de13b10b0bd", size = 324259, upload-time = "2026-09-08T13:26:13.989Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/a88215e7fcb395929d09045c9fea2505ef0af7130aad2674a6d03543b142/uuid_utils-1.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f0bcbaea1199ddc92cb94a0d7df151e4fbed558e6c00d900a214913f67a901f", size = 346937, upload-time = "2026-09-08T13:26:15.555Z" }, + { url = "https://files.pythonhosted.org/packages/1d/19/7f07c428461fb3923081065a6d38cb6782c5e08e87ee28cb84a61eaf2797/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4c4bf45ffb105c5a8d701bcbe4c1a0a28a55d29090fc6fcb0d1504cb9bd2b85", size = 501465, upload-time = "2026-09-08T13:26:17.038Z" }, + { url = "https://files.pythonhosted.org/packages/86/cd/72c265eb24499b9b57bf368a349a57d6ff0b8a979b879ed7828a41714df4/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:36eb692a959e54815edb0df9b1f566fcf889292f92e810a1019d2fee783ba830", size = 607865, upload-time = "2026-09-08T13:26:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/2d698113ecfd9b071191219fcfb98852d924dcf5387a4b00458863547a58/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71bd49db6f19d9dff7dfd560c81b72083455ab22ce2114837fca5b3adb2b790b", size = 564249, upload-time = "2026-09-08T13:26:20.01Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b7/399e029f06cd587a5719ff3dd59af7a34124ccb6a5f6078304391b6c015c/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d1890c89b50a70e3651db7a88aceba2f73c4a00d4bdca6c3c0c777480f69fefc", size = 528943, upload-time = "2026-09-08T13:26:21.579Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cc/6aa21ec6d99ff3eaa53673e23d06917ddfc1812a0361805ecf081ed9710d/uuid_utils-1.0.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6d94d66a073d76dbb3662baa1a659f4b6f4c87cddb9e5ced611e93a4fd34f55e", size = 99237, upload-time = "2026-09-08T13:26:22.97Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/c6ba25b7153c4257c7629e7eab446830cc9b72c73772e687253b00d23e95/uuid_utils-1.0.0-cp313-cp313-win32.whl", hash = "sha256:a33de2ae30c8f5a0b82294ea979f19951c01f39a7800c9806b50d7a1b301253c", size = 170584, upload-time = "2026-09-08T13:26:24.495Z" }, + { url = "https://files.pythonhosted.org/packages/e9/67/c9815dce0216be38b0eb89fd1bf198657649422679b28d93697ba477bbb6/uuid_utils-1.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:3474e58925c9779012318785a82a0e883b5f99cbf1cde75f60c28b9252a3840a", size = 176648, upload-time = "2026-09-08T13:26:26.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/55c869c409c9b372d5e8a9ed709c4937f3030df34833644a599db0e70d91/uuid_utils-1.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:b8f6a3a66703943f5cfcd317b77565fe9b7c5d8eeee50282de673bdeff1697f6", size = 174877, upload-time = "2026-09-08T13:26:27.515Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/6dfd6641e312d8d714c64ce19540b95d79caaa79bec31b41f35689bcd921/uuid_utils-1.0.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1501e0aef7e2ea759b6aa3967395896d803e8073d360ef95f62e18c52c5730f4", size = 562333, upload-time = "2026-09-08T13:26:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/ee/01330e815a75a5fe65f156ef194f2aabc214d3d8129e62c814579bde3040/uuid_utils-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:06f5da96427cfb28dc374a212ffe2d9becf5d099be9ee6b06600f77c31acb016", size = 289324, upload-time = "2026-09-08T13:26:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/34/d2/0a5b7baba5590460c610f436c9108e5897010d745de423246192a5c5f2c5/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:874b4fbb841197d94f256db3058ede8687e5293f3838b988346aad1f9af46b12", size = 324645, upload-time = "2026-09-08T13:26:32.546Z" }, + { url = "https://files.pythonhosted.org/packages/39/4b/141d0547f40f1a56888e186722431971b2978ab82f001445d2aca8c0e293/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570de62607ca78bfb5f5ae09871aaeb7d5d3f41b625663df124fbd1f56a87a98", size = 334452, upload-time = "2026-09-08T13:26:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ea/342fbc8bcbf07cc3c137a01d6a4ddbe622b476120bb7c2a4be5c1699ecdf/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b87289c3e9e1ce8a6849d8754cbbbe06b96cade7b72261675eb0d5444955d935", size = 448997, upload-time = "2026-09-08T13:26:35.87Z" }, + { url = "https://files.pythonhosted.org/packages/88/da/6451810f642abeeb158b7db39104d55a6df729fa9068ab946dca72c9a7dc/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0538babadda38ce86196315a710b10c7928697e33cc4ad01573aecadad9043c7", size = 326083, upload-time = "2026-09-08T13:26:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/96d2a6b9b6dd3d8ec52d3f00c21aa3ce94c7775d4cd1796655393b996d85/uuid_utils-1.0.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0482b3f41f9c9f5c2a59f865de8a9498e0a8d82649955a2b5ec5ceb2793ddb1f", size = 350628, upload-time = "2026-09-08T13:26:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2d/f0ab13421101c1917a3a969d16aa5b998643446a8c0a5376b2909bc81d50/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8af4a4166e84be69ba78851bcfb529302845f0551d2be87499e6f7c8859f9bc8", size = 503168, upload-time = "2026-09-08T13:26:40.518Z" }, + { url = "https://files.pythonhosted.org/packages/08/fa/1a31c44665623562def4b1991c7e2951ba3fa7678390d117aafdac45eb90/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9ddba367907dbb0892583dfd6a5989dda9257428b4022c86def98cc164db6096", size = 609820, upload-time = "2026-09-08T13:26:42.32Z" }, + { url = "https://files.pythonhosted.org/packages/df/c3/a441d1ade251b19b31728fe44aaba22d6d135dd7b90e4b6318d701ee62ae/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0e2e87a57cf1346786cd8067c1ebee1a0991996686a8fd9fa62db0d377a219ce", size = 567491, upload-time = "2026-09-08T13:26:44.309Z" }, + { url = "https://files.pythonhosted.org/packages/83/33/39a1d3a4d7e223aed19d61aeb3110d27eef2a640dcee0ab538f6b1821045/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d892757ce2dbe4224a4fe5e589b9d12cefc77765c8d388b8f041433edec63d4", size = 531390, upload-time = "2026-09-08T13:26:45.886Z" }, + { url = "https://files.pythonhosted.org/packages/c7/01/f9139035e3fdbd9e395c23c60b0b5d97462930130f5bac68e4a4c1cf378f/uuid_utils-1.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:72db71871915b8048e63444c91587a28c92eb9ac15dba94568bef20ec350e238", size = 101139, upload-time = "2026-09-08T13:26:47.504Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a3/d914af7a3504a086eeb525625ead867b47731ce28e1cffbd995a6f923a51/uuid_utils-1.0.0-cp314-cp314-win32.whl", hash = "sha256:fda1280fdbc110b7e9166796e30974f3400bac8e1fe135c9da00e96acc7c51f5", size = 172895, upload-time = "2026-09-08T13:26:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/cd/18/6c1700d7d637a4fea48f83b83ecd395bd66e685e2d6afdaf113be115f552/uuid_utils-1.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:7126a2b7a43ae6abdb5143aa228ebfefb8e436cc4bcbf91fbf080cf06c26f5cd", size = 178287, upload-time = "2026-09-08T13:26:50.325Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/02cd47c857bce282434b02732c5ee601e76a2c15db0590fbb130c2a8c2c4/uuid_utils-1.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:ac789644b2b50a5fb4c670df304f03259d9c2bec7c3b46facbfed760cd0249a3", size = 176454, upload-time = "2026-09-08T13:26:51.776Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bb/805a581bac06982b94ce786092cdd48e7aaa3b4d02bacee7980129933979/uuid_utils-1.0.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7acf1911189491b976c0a55889c8420cf4040c242a3c3d46ce2bf7e654a2398e", size = 563300, upload-time = "2026-09-08T13:26:53.335Z" }, + { url = "https://files.pythonhosted.org/packages/b2/36/d5d54eb9b8673a0e410bca3e27cacfa153d6547635c13ac08e4625ed4a44/uuid_utils-1.0.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c2c3ca406fee6bae70aaed1341ce22d3dc50b34ab2d965174dbd3a69c9e5b770", size = 290189, upload-time = "2026-09-08T13:26:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/44/b1/2cb5fafc86d6dab4269e9bbebb125e204bc3f5d1003ef48741d87f97044f/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2852ae83fd158fbb7b842bdd5119010a3d101b3fc1d80bd6f2353469e3b054", size = 324986, upload-time = "2026-09-08T13:26:56.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/02/1f9632c4c8ba04c00f41323e68221757abb3af32d8da706d2191001d212a/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b6c5661b63ca6b83c5bb4d916b00620883334ee639f61065ef9a6036e4f406f", size = 332704, upload-time = "2026-09-08T13:26:58.005Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9b/6682303842806da6874022e706325601e86780dc4cc421d2a3ab1c4bb98b/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73facd346472b56ffec54d6dc05a0506d7ece0e7d9c6bb53e6bf711cab7e7f07", size = 448017, upload-time = "2026-09-08T13:26:59.616Z" }, + { url = "https://files.pythonhosted.org/packages/44/ca/4213f7bd913695b18cf6a280204368cc4869aa89eb6005b93d2a0013fe09/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad58a78d06c232f1c817f31d4b974ec59608faeffa4b07b859fc023f8add1d17", size = 327722, upload-time = "2026-09-08T13:27:01.078Z" }, + { url = "https://files.pythonhosted.org/packages/26/a8/691f91c8d28be9bc567983bdef53c49021ddfd3582fbdc4f9a3682895ac2/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a1417bb6abce4039a1b4a8a4d83e6ddaa694809297f2d167bffea077cd7e8aa", size = 350181, upload-time = "2026-09-08T13:27:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/96143792454351baae7d5eafe877c67c9a3c1d9f5453ffd8a817f66e0de7/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d9c6ffb566f29d741c6e59a367d4ddf073fbd3decd50c43ef99aa3a3285b691", size = 503063, upload-time = "2026-09-08T13:27:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4d/333c251382484bcbc41e83046ecc95d7e6ece767cabeaf79af69d779f23f/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d826362cacccd6ca52140877c6b84fb0ea6544b9cdf8bd860eb877f76f16ba4d", size = 608178, upload-time = "2026-09-08T13:27:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/69/db/57f42643e324d1dd4b8764e9f51f63cccc97e0ea9dfdfae4763e616e43fd/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:87f68b983e73ef8aacb4fbcfabda37d2d6e0431c54a209c1853bc3487e3237cb", size = 566908, upload-time = "2026-09-08T13:27:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/c5/54/deb1dfd1634df28de05d73ee874fc8cc2745ba8b71776bddea039f6954bd/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f18787481b251b700bce778a8906b7683478fd4895878ec4b38e150d481fbaab", size = 532506, upload-time = "2026-09-08T13:27:09.425Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/0e811a8817a1bb4e974b42ffbeac3eb8a01b6c75795c5632d8f078b69781/uuid_utils-1.0.0-cp314-cp314t-win32.whl", hash = "sha256:7217beaa4650030bc225d21583fe6105dbe33271b8cb994bca367cdc32dac47e", size = 172683, upload-time = "2026-09-08T13:27:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/fb2336e98432cc13bf26765740bef8d15951e62e3e83e103ebd081748923/uuid_utils-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a8cc8dbdda2615d4aef270bb0f1a931071b4211a21e18dcbe734143beab1780", size = 178478, upload-time = "2026-09-08T13:27:12.361Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/cdc26ca2af2fec7ecd7154d7a611237a56d39bc58ba8562f6cd0800f3324/uuid_utils-1.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b4920f5abfe2c84c5b2fc3e6b6063f8c7032289adb92d578750e0c1937ee77da", size = 176067, upload-time = "2026-09-08T13:27:14.183Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fc/83787071a199470a2269558b6874485a2fd4f2ed845232ef99bd1d08a0ee/uuid_utils-1.0.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:460c624577d490343df0f35c14bb4871f41738d0584c3afba91f2a8eded8dbe1", size = 567122, upload-time = "2026-09-08T13:27:15.896Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9b/356e7e17851693268303c7309620d24b569fe7dce1d14b1663c6f17e2bd1/uuid_utils-1.0.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:288690101bb71d79f2d5ca236ffe96306d5c131175e3af21370c0243520c9cf5", size = 289245, upload-time = "2026-09-08T13:27:17.446Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b7/98df02e50afbae0bd076386e8ec412824da87274d1a667d82d9abec9bdfb/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5124d050465317c795befc6340210239b33cea034d55271e7a8d6b53e56817b0", size = 328652, upload-time = "2026-09-08T13:27:19.107Z" }, + { url = "https://files.pythonhosted.org/packages/22/f4/0ed779f46b5b52e0fd4ee5d8168bb81853cecf03f108e8b1f5cc8e1d57be/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0938813da8301296bd34bd707cec47bb718f90f9f31aebbef607ebef0f3d5da", size = 337027, upload-time = "2026-09-08T13:27:20.947Z" }, + { url = "https://files.pythonhosted.org/packages/56/70/29df489a5736544dd8583ee8e84e18b9051f1c04f8b83535727abd0473ae/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed2a8b86e77bc33a46c54345bb8648c4c6773cce9e20b6cd9e629a8f482718c5", size = 451780, upload-time = "2026-09-08T13:27:22.469Z" }, + { url = "https://files.pythonhosted.org/packages/ee/05/ce99a3008ed2678c25c45137053869fccfd83fdd54976fd1defc6265485f/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7e78528fdba973fa37110221bdc01df0bce78807496c0898dc384555ba3c9c4", size = 329155, upload-time = "2026-09-08T13:27:24.017Z" }, + { url = "https://files.pythonhosted.org/packages/31/01/86ca857663951ca4287390047e27dde24e248de3b16007b03dd088b719be/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90c14789ce9e04a03111cfec963d1b5f988a665573b1d94b6fcfe325ea2c3fbe", size = 355746, upload-time = "2026-09-08T13:27:25.573Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d9/612687380487212a891ff369ba64afda76c7ec64d39d7f2eea28f2b36737/uuid_utils-1.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c59fc1a184b34e00a60ed77e8d2913a1e7f197af912206000f94b53e79d1f64e", size = 181648, upload-time = "2026-09-08T13:27:26.987Z" }, ] [[package]] @@ -6566,52 +6774,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "watchfiles" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" }, + { url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" }, + { url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" }, + { url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" }, + { url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" }, + { url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" }, + { url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" }, + { url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" }, + { url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" }, + { url = "https://files.pythonhosted.org/packages/cd/eb/289fec71eba51d6c3ef3a28c81f7213f4e860084ffae5abc866933c564f0/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1", size = 396932, upload-time = "2026-09-21T09:08:50.217Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/a3c3f557992a20a5bb24607f0da44d39cf6d6ba3f8f6ed7cb31f6c73371a/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c", size = 394852, upload-time = "2026-09-21T09:08:51.412Z" }, + { url = "https://files.pythonhosted.org/packages/55/65/d45763a5e1108a65af85a217af2930ebf1081c73fd8169d3f75e586992bb/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef", size = 451789, upload-time = "2026-09-21T09:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/02/96/165c6e540bf765ca1115062017adf861d0816525f02464ba71313fe7cf92/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f", size = 456919, upload-time = "2026-09-21T09:08:54.372Z" }, +] + [[package]] name = "wcmatch" -version = "11.2" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/07/b7a9162a804bc0f7d03fe84c93e374d3e1c4d828043d5f9b797f7d4e30e0/wcmatch-11.2.tar.gz", hash = "sha256:1605bde546fbde4c063628c7d89eda486909fd4337e51d73a873987fbcb2554c", size = 134939, upload-time = "2026-10-22T00:37:57.622Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/cd/0e028d8b7d906d51acdac537334fbe85f8284f2a8147d48bc9403af77a0d/wcmatch-11.1.tar.gz", hash = "sha256:f2b6f0fad6f5e2571498723fcb3c8facdd0fba0dcc139f31d32f592eb648153e", size = 129091, upload-time = "2026-10-10T13:10:54.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/96/b6cebd4bd75320743a291fed494c79f07c01b58fa9b187d12aeb5110f002/wcmatch-11.2-py3-none-any.whl", hash = "sha256:952bab83c75c233836007a69550a3a8753cf06964c1eb3b84ff11ad5bff1c272", size = 44361, upload-time = "2026-10-22T00:37:56.293Z" }, + { url = "https://files.pythonhosted.org/packages/1e/22/c4adda5a3754b7fc9429a544a0673fa6922fdd7808eab09abe18616f6e38/wcmatch-11.1-py3-none-any.whl", hash = "sha256:2863afee97a7dd62e64f7feada864848c1a5e31cb331b3aa530fa1300d2d5d3c", size = 43417, upload-time = "2026-10-10T13:10:52.939Z" }, ] [[package]] name = "wcwidth" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/4a/82d61476a1ccc20d43d0805724c5de4baa4971131a8db1816705ef53fe16/wcwidth-0.9.3.tar.gz", hash = "sha256:dfe7cfbd4cb59db9fe0931bd6091ab4a95b3acc5d56bfb50fa9fef0ccba5dc5c", size = 956320, upload-time = "2026-10-20T16:02:43.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/6d/eec61b5edeb88e3ea2a714184eade38335722c4246d94be567977fc20caa/wcwidth-0.9.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b119ef60177a92b4f298b80e98464c2922a1fba3ed77fe9a2bcb69edf1b9af4", size = 605400, upload-time = "2026-10-20T16:02:02.523Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3c/714eba03905a23a46277cc96d3db8da6bd22d2cc0d09a4dead60a2bd1757/wcwidth-0.9.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aab90ab489850f87633922c3064f6d0442d8f6ae5407a62f82bdb1f424179c05", size = 610124, upload-time = "2026-10-20T16:02:07.256Z" }, - { url = "https://files.pythonhosted.org/packages/90/ad/8566e7cc712bea55eab0bb6d19581b7c727a58fcdd55f02365b309aaaccd/wcwidth-0.9.3-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e1cbe18c18d152e1e68e4e2561dab6c2bc988f8fd337a1fb2e1e0809c3fa568", size = 762700, upload-time = "2026-10-20T16:02:17.508Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7a/4e26574d032a1709bb1fd7d95d406b7b8f87363c60305a101e6e2d9285f5/wcwidth-0.9.3-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c12abd75ffcbad417e7770a6feb82413c102ccce5c435068c39a82e2218b00b3", size = 771850, upload-time = "2026-10-20T16:02:11.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d6/48dad66d577417ef8594000506fa8619d45d7282e20b4cdc916fcc68f95a/wcwidth-0.9.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9ab8bfee107e08de09e15f9f6afb9fe0f64c8d64fd296d0d8557bafe71ea3f93", size = 770047, upload-time = "2026-10-20T16:02:19.092Z" }, - { url = "https://files.pythonhosted.org/packages/3f/61/5f1e02e588d289837d5312478cc9f0e7bca93f4c8a77807741f79fb6a31c/wcwidth-0.9.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f875a4c9e62df2ecfa70d99b24c551f2245609858e179c3a40e74631b2d08a4", size = 782210, upload-time = "2026-10-20T16:02:12.753Z" }, - { url = "https://files.pythonhosted.org/packages/e9/11/0da2720cba3f917148448c9bf73f3079d71f918308ee9e4d737a0b7acfd8/wcwidth-0.9.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:70a2ca239b85a219324968993c76c117fe423d4e1a7c9d860c999c66192f2570", size = 783172, upload-time = "2026-10-20T16:02:20.663Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/b79407036ca4543bf708a6d4ca561f78620dc1e5443e685e094480c37753/wcwidth-0.9.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:537ea00b6792bb243cd09582322c54dbc848879e5d34ea4c5fee0147529685ab", size = 784806, upload-time = "2026-10-20T16:02:22.164Z" }, - { url = "https://files.pythonhosted.org/packages/3c/86/ada6c2244b08a86c871d42f7cd85b5226515b5f3fa7e604d1e6e45590275/wcwidth-0.9.3-cp310-abi3-win32.whl", hash = "sha256:f2f943c4ef14e9baba730740265f4e5bf5fc8473550e36cd92828cd5663561a6", size = 593154, upload-time = "2026-10-20T16:02:34.301Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6e/46363d972b2b9f002fbfc3bf8e9ab0ff89f139c935a5f21d22016e7a2018/wcwidth-0.9.3-cp310-abi3-win_amd64.whl", hash = "sha256:569a0cdb826d3097b6dbacfaa45aa0002d72a2ba6fa8a63251ea43ac4dd7b6a4", size = 597685, upload-time = "2026-10-20T16:02:35.748Z" }, - { url = "https://files.pythonhosted.org/packages/69/3d/5583cac85e233514c3906d5476b6a74160bc4dc82729dabef3485dbf3d52/wcwidth-0.9.3-cp310-abi3-win_arm64.whl", hash = "sha256:80557db1f77148b78e1b3c2a9cc4ebc4feabef42aaf69a555565057031c32367", size = 598774, upload-time = "2026-10-20T16:02:30.016Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/53e0838babc6b87505b9504ce62b72c79403642f88d3e568e05d0ef04825/wcwidth-0.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6f3e855d3a09af8b7b6d56e76bc270681032f8a37d527a8d5707a441958e3730", size = 606640, upload-time = "2026-10-20T16:02:04.01Z" }, - { url = "https://files.pythonhosted.org/packages/f0/fb/27c94facff5c4a39b954a41fac20c238647f0aad32452d0d9e33fc05e852/wcwidth-0.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1bbc8ebf133422cadfefea95a9f89d6c9e9a1ae19930bef7862a9b59b20bbad", size = 610956, upload-time = "2026-10-20T16:02:08.764Z" }, - { url = "https://files.pythonhosted.org/packages/38/61/38e6236356d25966d34636ef6f49ee83a66513e051c4a3b1e19c8b9ec806/wcwidth-0.9.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7a95d8c784cbccbbcb808eaadfd156cd282b2bdd95e89b32505e3443381ce61d", size = 775967, upload-time = "2026-10-20T16:02:23.765Z" }, - { url = "https://files.pythonhosted.org/packages/7b/87/c8f9295cd14a058dc27be0360fcc5ec70ce71b0d2e6a018871f585f23126/wcwidth-0.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a44acbb23b525070a0b8c343dd7cab7a4020aff73b38804f7a66d6a66e16e3fc", size = 787147, upload-time = "2026-10-20T16:02:14.095Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c0/238c135e283b1b0e16fef8b082d432dda1fe7e568d6dd85d5c7fc5e83859/wcwidth-0.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a7c0405a3bf3c564e481d6b2d55c99cc231bb68cf8f4a0995944bbce5ebf1a5d", size = 784548, upload-time = "2026-10-20T16:02:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ed/33e3080fe32f6f8f86334b2c5adde243b9a3a3a46cef97694dbed73b9dc4/wcwidth-0.9.3-cp314-cp314t-win32.whl", hash = "sha256:457f986e4867111cb037ccbf0d97ae100b1ecad030d43a020e7207f1e29649d5", size = 600761, upload-time = "2026-10-20T16:02:37.097Z" }, - { url = "https://files.pythonhosted.org/packages/27/62/6684a59147c1f86d58d0138f3109f506a6761b815dcb3d86b74d9af3a723/wcwidth-0.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:f11789e520ade8017fa93b01a4b8985571bb433eaefc2f857b57e36875e2e1d8", size = 605316, upload-time = "2026-10-20T16:02:38.513Z" }, - { url = "https://files.pythonhosted.org/packages/94/24/3ebcfd772d273e43e28447dd1946d88b8625db57b6a23496ea4eb9e1dbb7/wcwidth-0.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:eef39f3260991d8767189d296f0116fa4b45ad9c3a9a16d5af9187fb66b27564", size = 606392, upload-time = "2026-10-20T16:02:31.333Z" }, - { url = "https://files.pythonhosted.org/packages/76/fe/f90b9444bbc2d566634f91756bd11c2bd39d50c2c9077551711b10d8d03f/wcwidth-0.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:52a13869704d43a4add59c68ebc892198e53063883114783bf0b7457e250e3f4", size = 606655, upload-time = "2026-10-20T16:02:05.816Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/edffb23938f4e27c28400c7d8d5a8715381d77f5db96bf80cf5dd6687f88/wcwidth-0.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9470169fcb4e5340269175ebb3642dd5bfc8f82da8c86f29fc1fefc6c2b9bc1b", size = 611217, upload-time = "2026-10-20T16:02:10.252Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e6/0e4b5c81c238fac47eb66902f058a39ccdeacd7b2c5ab0c559066f203262/wcwidth-0.9.3-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c4531d93344441a7948e665854f0c40e3a8342ee106436d1531efa7a9fa7641e", size = 778155, upload-time = "2026-10-20T16:02:27.141Z" }, - { url = "https://files.pythonhosted.org/packages/f2/16/745439326544b2a6d65facbdf8462ee9d31e09c64830a764d2526c6c80d6/wcwidth-0.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:658dcbce546e7f28ee361f88e4a8e591dd56f46bff6084e11b4ad8310ed99946", size = 786757, upload-time = "2026-10-20T16:02:15.569Z" }, - { url = "https://files.pythonhosted.org/packages/08/a4/47193dc9902c30e187d81feaa7a5bc2672c41fe07093ca1ce5381395b85c/wcwidth-0.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9362be098c43d076aa43754face3cc0180fa81cea9ba304240b15490f2617e8", size = 785088, upload-time = "2026-10-20T16:02:28.784Z" }, - { url = "https://files.pythonhosted.org/packages/11/6a/9727002b938c28ab4456fc43ba31a3159ebff040021ce8dbd70e904c7cc9/wcwidth-0.9.3-cp315-cp315t-win32.whl", hash = "sha256:16d09df90b3a8f799ea14c726a41d9b906c3104f95f46d158e1ebe45cf4d547b", size = 600787, upload-time = "2026-10-20T16:02:39.766Z" }, - { url = "https://files.pythonhosted.org/packages/7c/db/0e24c2ff1e82cf298dd40a29e022da31b79be2589f0b8d865488e49140e1/wcwidth-0.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:3e3e31d6c17d6da3051f7eb854d5e4bebdc52a08a7a52adc2da6f15af51304e8", size = 605298, upload-time = "2026-10-20T16:02:41.479Z" }, - { url = "https://files.pythonhosted.org/packages/84/71/b470bd1dec14cbe6cee0c2e142787a8d99ac01711d5a1b533dda90754b48/wcwidth-0.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5d3b0b6c37f40e8d8298941ec373a426a892bb9a514328abcc9226cdc60ae6eb", size = 606346, upload-time = "2026-10-20T16:02:32.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/12/e10aad98050389ef674c9f69409e38ae086c2104912644e1f9143b394301/wcwidth-0.9.3-py3-none-any.whl", hash = "sha256:e3c9069c54a2da0bf4f6c7e65a735cb1da0f47601a15cb5fef7e4b6ffca4cf60", size = 301629, upload-time = "2026-10-20T16:02:00.861Z" }, +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz", hash = "sha256:ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b", size = 955039, upload-time = "2026-10-05T00:24:05.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/1e/4532a81fb9dfbf4114a816775e0a36c3a64ee1d1f4bba2094e2da50be5dc/wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07", size = 605343, upload-time = "2026-10-05T00:23:22.649Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/cb6940e81134b7ed25fa312ee9ab536a63db0793b149f88a90e603ceace9/wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17", size = 609960, upload-time = "2026-10-05T00:23:27.049Z" }, + { url = "https://files.pythonhosted.org/packages/a4/80/15ad05d40bfa99155639fb9e13b3d77083aa0fab893c816db2543d29005c/wcwidth-0.9.2-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79", size = 762566, upload-time = "2026-10-05T00:23:38.322Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/b8ef7758003d66b60f093695831a86dcc726aac01ee6446ffcbda27b61e3/wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724", size = 771632, upload-time = "2026-10-05T00:23:32.448Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389", size = 769763, upload-time = "2026-10-05T00:23:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/285f862826f721964ec7c42f81dc53d23afbd723a0f4cd989651f8218e25/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7", size = 782134, upload-time = "2026-10-05T00:23:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/c2/2d/64aa54882a5d556d3654c1f926d9118b797461033e23a158409941a37c8f/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2", size = 782851, upload-time = "2026-10-05T00:23:41.974Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/52389f6de7fe2e9c14ceb8253dd99034bd86e1c87847ea3c100a97dded9a/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04", size = 784621, upload-time = "2026-10-05T00:23:43.449Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/20225500a076ace27bbcc8a6fd7c55125133c57a618816c7b7b8b73070b1/wcwidth-0.9.2-cp310-abi3-win32.whl", hash = "sha256:ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4", size = 593123, upload-time = "2026-10-05T00:23:55.953Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/b0690f55ea0483530a18bac917fbadbf54f35122510446fc370f5f1c2453/wcwidth-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec", size = 597827, upload-time = "2026-10-05T00:23:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/6ecf4e9e268ab1a4ec617ffcccc2ee4a71301625f5490912dbaba462fa9c/wcwidth-0.9.2-cp310-abi3-win_arm64.whl", hash = "sha256:b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa", size = 598415, upload-time = "2026-10-05T00:23:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/4e/41/549eef1ab767032bdbdc1f0ab655d404b082b1e9a1dab1361dbba90f64ed/wcwidth-0.9.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eda88ffdc97c0fbf193d407114f2c7a54b379f67f6e52a7531ee3b9fe749eca7", size = 606555, upload-time = "2026-10-05T00:23:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/9b/64/a875ed7ea71cacadc0ae11b5fd3fac3486efd58bb25e67a7344248dceadd/wcwidth-0.9.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf361c8705576760623b4724ae564666d73b016f9a778bcfd1c7345378ef4ec", size = 610829, upload-time = "2026-10-05T00:23:28.563Z" }, + { url = "https://files.pythonhosted.org/packages/c6/98/513095e484fe79b6f2613d6a72f855f5d56b65e15c215c2a6746fbc638f5/wcwidth-0.9.2-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:97b878d1e158da5ed9ac5aac53fa3a55e282103af6a09ec353865613d1a31a76", size = 775819, upload-time = "2026-10-05T00:23:45.116Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/c02f3eec57224731e78f84b68e272250f784b6205acc7e0dcef6a7c23a0e/wcwidth-0.9.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:59dab4049cbd982b478bca098528df2c79a9160636a3a163ffebffcbd7d1b892", size = 786915, upload-time = "2026-10-05T00:23:35.323Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/b0529a79bac3fe8d94f32b4237a13dbc3f955508753f6a6f06c73d679dc2/wcwidth-0.9.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb08ceb501d6aaf94066c3ee122dd825b152df40ff0bd0df4dc27126233b948e", size = 784186, upload-time = "2026-10-05T00:23:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bd/6357c84ca9a734bfc735b7c48dbe21336b3777fab8a4101d14976dfe49a7/wcwidth-0.9.2-cp314-cp314t-win32.whl", hash = "sha256:8b4e381590b9b7390e07e22b2c0c1bb96ce50e1d2243c866d9387600362d51ed", size = 600791, upload-time = "2026-10-05T00:23:59.398Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/037591ca18d897cc2179559dde72e6efc6ce0c90e9cd1e6bca4e87c38b4b/wcwidth-0.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2f7b3bba5a5d5f31fc350fd36ce5b84b693c83b7eb95ee630b720da5a5ce06f", size = 605463, upload-time = "2026-10-05T00:24:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/d0/07/c9d96e106d938d26f7ab639bc80b8199359a1645ba6e3498413313ab6f38/wcwidth-0.9.2-cp314-cp314t-win_arm64.whl", hash = "sha256:734aa9405b321d1042301aa19c943c4731ee9e3460e4f8feea3299c064c97a14", size = 605936, upload-time = "2026-10-05T00:23:52.765Z" }, + { url = "https://files.pythonhosted.org/packages/82/8a/a28d61d910005ac93dfe48be3a0ebaa49352d88cebd25323e69e6ff2f4a8/wcwidth-0.9.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724", size = 606575, upload-time = "2026-10-05T00:23:25.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/c2/a3c66bd32766c8f4d6dc47d572532ba014fe5be30489f2576aff7cada363/wcwidth-0.9.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2", size = 611045, upload-time = "2026-10-05T00:23:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/ec/8a/d39964f8f8c019d7d439b9b501d3e7bb42fee69f00354040ba0b27b5824c/wcwidth-0.9.2-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c", size = 778143, upload-time = "2026-10-05T00:23:47.7Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/525da13e8f9ff7b5b4e74ec6f8d68bdee63905796972e086c6b1b96670d2/wcwidth-0.9.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d", size = 786630, upload-time = "2026-10-05T00:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9f/d6a0c6df354b9d93466548a65cbf4ffcb48c719bbd307504cf3e76740837/wcwidth-0.9.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270", size = 784839, upload-time = "2026-10-05T00:23:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d7/3021feed1ed7926021ec134943ad3b24a2f7ea742cc9976461171482ed77/wcwidth-0.9.2-cp315-cp315t-win32.whl", hash = "sha256:0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b", size = 600845, upload-time = "2026-10-05T00:24:02.497Z" }, + { url = "https://files.pythonhosted.org/packages/63/80/6a03356d8ee38261e3a78cf89ee03d8e7f12c572d969237be00869e2dc73/wcwidth-0.9.2-cp315-cp315t-win_amd64.whl", hash = "sha256:33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9", size = 605441, upload-time = "2026-10-05T00:24:04.052Z" }, + { url = "https://files.pythonhosted.org/packages/0c/48/1a308a86a833fd12ff7a08d0d2491ff4a72c8a92d12f5ead8317630f771e/wcwidth-0.9.2-cp315-cp315t-win_arm64.whl", hash = "sha256:48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8", size = 605885, upload-time = "2026-10-05T00:23:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl", hash = "sha256:89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e", size = 301667, upload-time = "2026-10-05T00:23:21.097Z" }, ] [[package]] @@ -6887,33 +7138,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "zensical" -version = "0.0.71" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "deepmerge" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "pathspec" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "pyyaml" }, - { name = "tomli" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/afe0a4859c2c1dc2a0d770d6ba5f89171f642169028580156e3080d11b7d/zensical-0.0.71.tar.gz", hash = "sha256:3b206f150eb38d2885158e093113460a5e667847cf36915119c21b20776cee18", size = 4328098, upload-time = "2026-10-16T14:19:36.83Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b3/0e3f822ab105f503504a0149a9cce6da59fa9626e9c68243c66b02e51efb/zensical-0.0.71-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:57cad23c56a98d87a5eefa7948a98b6ecb479dc99fd7af312cd146555f55f060", size = 18136451, upload-time = "2026-10-16T14:19:12.764Z" }, - { url = "https://files.pythonhosted.org/packages/5a/75/71dbdc5106a4153b8daed4b0161965dce9e3333b9e27bd9cf4470dac07bf/zensical-0.0.71-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:81c069cc667946e0a505cc01b99d5ba44883c57f51b95bd5a38003007c5cb6cd", size = 17670227, upload-time = "2026-10-16T14:19:15.679Z" }, - { url = "https://files.pythonhosted.org/packages/84/bd/86f0d0ae73ce27d72bb49575c2a91da9d686916cf06980b03b2b480ab688/zensical-0.0.71-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae388415698cac874e662e667fc0683833cec747607c89037b7e9ad9828830", size = 18167627, upload-time = "2026-10-16T14:19:18.043Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f4/4f341c2dfd6fc7cf79c87287b835916570a4333e9dfef19374ba0ed8d842/zensical-0.0.71-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc8ed534717b61f285dc217f165f901f8b8a6142ef37cdd5544976cd99b5720c", size = 18651322, upload-time = "2026-10-16T14:19:23.773Z" }, - { url = "https://files.pythonhosted.org/packages/2e/12/4e3c0430d5646509967e82340df51d9d83c5a5fc232302fd094d938171d5/zensical-0.0.71-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e9e39aefdca4b3383b43014e117b27a1ca9f9ac4b25eefa9195099e5a566e2f", size = 18341323, upload-time = "2026-10-16T14:19:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/61/14/797afbdbeaadd671c44cd3d9740d1f67ecda70a931133c155542a4d6b5cf/zensical-0.0.71-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d22b8924e5145d94bf85b81bd2a8afaeb32e78c8a1ddeb7f4201fc8902547221", size = 18908964, upload-time = "2026-10-16T14:19:29.382Z" }, - { url = "https://files.pythonhosted.org/packages/90/17/395bbcc02c35e3f132d4c083be3cb0c5eeda936c2a32d800d97d9d810183/zensical-0.0.71-cp311-abi3-win_amd64.whl", hash = "sha256:2d3c2f6360374afc12cd0cfbf087921c6ac4a069b4c1e60a4c80491801dda146", size = 18838514, upload-time = "2026-10-16T14:19:32.195Z" }, - { url = "https://files.pythonhosted.org/packages/96/33/af4aac6d08bb63a3de2549e3582467c9277083b033ce18d92ae776514009/zensical-0.0.71-cp311-abi3-win_arm64.whl", hash = "sha256:433c5a6fecae7b6b5014b7efbc52bb5e0776d715afca60e2832a12162e7d571a", size = 18253924, upload-time = "2026-10-16T14:19:34.521Z" }, -] - [[package]] name = "zizmor" version = "1.31.1" From aaace8f005fd5b676ecba8394ce101a852561c93 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 15:57:40 +0000 Subject: [PATCH 02/35] test(fixtures): modernize rustfs, gizmosql, bigquery, and spanner clientless fixtures --- tests/fixtures/rustfs.py | 16 +- .../duckdb/duckdb/test_native_storage.py | 10 +- .../spanner/spanner/test_batch_write_api.py | 5 +- tests/integration/fixtures/adbc.py | 206 +++++++----------- tests/integration/fixtures/bigquery.py | 2 +- tests/integration/fixtures/spanner.py | 63 +++--- 6 files changed, 126 insertions(+), 176 deletions(-) diff --git a/tests/fixtures/rustfs.py b/tests/fixtures/rustfs.py index e0ea17ef2..39e70d972 100644 --- a/tests/fixtures/rustfs.py +++ b/tests/fixtures/rustfs.py @@ -7,6 +7,14 @@ if TYPE_CHECKING: from pytest_databases.docker.rustfs import RustfsService +__all__ = ( + "rustfs_endpoint_url", + "rustfs_filesystem", + "rustfs_fsspec_kwargs", + "rustfs_object_size", + "rustfs_obstore_kwargs", +) + def rustfs_endpoint_url(rustfs_service: "RustfsService") -> str: """Return the S3 endpoint URL for a pytest-databases RustFS service.""" @@ -45,14 +53,6 @@ def rustfs_filesystem(rustfs_service: "RustfsService") -> Any: return fsspec.filesystem("s3", anon=False, **rustfs_fsspec_kwargs(rustfs_service)) -def ensure_rustfs_bucket(rustfs_service: "RustfsService", bucket_name: str) -> str: - """Ensure the default RustFS bucket exists and return its name.""" - fs = rustfs_filesystem(rustfs_service) - if not fs.exists(bucket_name): - fs.mkdir(bucket_name) - return bucket_name - - def rustfs_object_size(rustfs_service: "RustfsService", bucket_name: str, object_name: str) -> int: """Return the size of an object stored in RustFS.""" info = rustfs_filesystem(rustfs_service).info(f"{bucket_name}/{object_name}") diff --git a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py index 16364748f..611ab6737 100644 --- a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py +++ b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py @@ -18,7 +18,7 @@ from sqlspec.adapters.duckdb.driver import DuckDBDriver from sqlspec.storage import StorageFormat, StorageRegistry, SyncStoragePipeline from sqlspec.storage.registry import storage_registry -from tests.fixtures.rustfs import ensure_rustfs_bucket, rustfs_filesystem, rustfs_obstore_kwargs +from tests.fixtures.rustfs import rustfs_filesystem, rustfs_obstore_kwargs def test_registered_filesystem_transfers_without_cloud_extensions(monkeypatch: pytest.MonkeyPatch) -> None: @@ -167,7 +167,7 @@ def _local_config(service: RustfsService, *, wrong_key: bool = False) -> DuckDBC def test_local_object_success_missing_and_auth_errors( rustfs_service: RustfsService, rustfs_bucket_name: str, tmp_path: Path ) -> None: - bucket = ensure_rustfs_bucket(rustfs_service, rustfs_bucket_name) + bucket = rustfs_bucket_name key = f"duckdb-native-probe/{uuid4().hex}.parquet" path = tmp_path / "source.parquet" pq.write_table(pa.table({"value": [1, 2]}), path) @@ -202,7 +202,7 @@ def test_local_object_success_missing_and_auth_errors( def test_arrow_and_native_copy_replace_single_local_object( rustfs_service: RustfsService, rustfs_bucket_name: str, file_format: str ) -> None: - bucket = ensure_rustfs_bucket(rustfs_service, rustfs_bucket_name) + bucket = rustfs_bucket_name alias = f"duckdb_copy_{uuid4().hex}" key = f"duckdb-copy-probe/{uuid4().hex}.{file_format}" options = rustfs_obstore_kwargs(rustfs_service) @@ -241,7 +241,7 @@ def test_arrow_and_native_copy_replace_single_local_object( def test_native_parquet_append_and_overwrite_fallback( rustfs_service: RustfsService, rustfs_bucket_name: str, monkeypatch: pytest.MonkeyPatch ) -> None: - bucket = ensure_rustfs_bucket(rustfs_service, rustfs_bucket_name) + bucket = rustfs_bucket_name alias = f"duckdb_import_{uuid4().hex}" key = f"duckdb-native-import/{uuid4().hex}.parquet" destination = f"alias://{alias}/{key}" @@ -278,7 +278,7 @@ def reject_arrow(*args: Any, **kwargs: Any) -> Any: def test_native_export_does_not_materialize_arrow( rustfs_service: RustfsService, rustfs_bucket_name: str, file_format: str, monkeypatch: pytest.MonkeyPatch ) -> None: - bucket = ensure_rustfs_bucket(rustfs_service, rustfs_bucket_name) + bucket = rustfs_bucket_name alias = f"duckdb_export_{uuid4().hex}" key = f"duckdb-native-export/{uuid4().hex}-quote'.{file_format}" storage_registry.register_alias(alias, f"s3://{bucket}", backend="obstore", **rustfs_obstore_kwargs(rustfs_service)) diff --git a/tests/integration/adapters/spanner/spanner/test_batch_write_api.py b/tests/integration/adapters/spanner/spanner/test_batch_write_api.py index 4cd3538e2..ea4aa7875 100644 --- a/tests/integration/adapters/spanner/spanner/test_batch_write_api.py +++ b/tests/integration/adapters/spanner/spanner/test_batch_write_api.py @@ -5,11 +5,13 @@ """ from collections.abc import Generator +from typing import Any, cast from uuid import uuid4 import pyarrow as pa import pytest from google.api_core import exceptions as gcore_exceptions +from google.auth.credentials import AnonymousCredentials, Credentials from pytest_databases.docker.spanner import SpannerService from sqlspec.adapters.spanner import SpannerSyncConfig @@ -20,12 +22,13 @@ @pytest.fixture def spanner_batch_write_config(spanner_service: "SpannerService") -> "Generator[SpannerSyncConfig, None, None]": api_endpoint = f"{spanner_service.host}:{spanner_service.port}" + credentials = cast("Credentials", cast("Any", AnonymousCredentials)()) config = SpannerSyncConfig( connection_config={ "project": spanner_service.project, "instance_id": spanner_service.instance_name, "database_id": spanner_service.database_name, - "credentials": spanner_service.credentials, + "credentials": credentials, "client_options": {"api_endpoint": api_endpoint}, "size": 5, }, diff --git a/tests/integration/fixtures/adbc.py b/tests/integration/fixtures/adbc.py index e64821ba1..0d9b54c31 100644 --- a/tests/integration/fixtures/adbc.py +++ b/tests/integration/fixtures/adbc.py @@ -2,19 +2,32 @@ import functools from collections.abc import Callable, Generator +from importlib.util import find_spec from typing import TYPE_CHECKING, Any, TypeVar, cast import pytest -from adbc_driver_flightsql import DatabaseOptions -from adbc_driver_flightsql import dbapi as flightsql -from pytest_databases.docker.gizmosql import GizmoSQLService -from pytest_databases.helpers import get_xdist_worker_num from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver if TYPE_CHECKING: - from pytest_databases._service import DockerService - from pytest_databases.types import XdistIsolationLevel + from pytest_databases.docker.gizmosql import GizmoSQLService + +__all__ = ( + "adbc_duckdb_config", + "adbc_duckdb_driver", + "adbc_duckdb_session", + "adbc_gizmosql_config", + "adbc_gizmosql_connection_config", + "adbc_gizmosql_session", + "adbc_gizmosql_sqlite_config", + "adbc_gizmosql_sqlite_connection_config", + "adbc_gizmosql_sqlite_session", + "adbc_postgresql_config", + "adbc_postgresql_session", + "adbc_sqlite_config", + "adbc_sqlite_session", + "xfail_if_driver_missing", +) F = TypeVar("F", bound=Callable[..., Any]) @@ -34,111 +47,21 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: or "Could not configure connection" in str(e) ): pytest.xfail(f"ADBC driver not available: {e}") - raise e + raise return cast("F", wrapper) @pytest.fixture(scope="session") -def xdist_gizmosql_isolation_level() -> "XdistIsolationLevel": - """Use one GizmoSQL server per xdist worker.""" - - return "server" - - -def _gizmosql_db_kwargs(username: str, password: str) -> "dict[str, str]": - return {"username": username, "password": password, DatabaseOptions.TLS_SKIP_VERIFY.value: "true"} - - -def _gizmosql_connection_config(service: "GizmoSQLService", *, backend: str) -> "dict[str, Any]": - return { - "driver_name": "gizmosql", - "uri": service.uri, - "username": service.username, - "password": service.password, - "tls_skip_verify": True, - "gizmosql_backend": backend, - } - - -def _prepare_gizmosql_test_table(session: "AdbcDriver") -> None: - try: - session.execute("DROP TABLE IF EXISTS test_table_adbc") - except Exception: - pass - session.execute( - """ - CREATE TABLE test_table_adbc ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - value INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - session.execute("DELETE FROM test_table_adbc") - session.commit() - - -@pytest.fixture(scope="session") -def gizmosql_sqlite_service( - docker_service: "DockerService", gizmosql_image: str, gizmosql_username: str, gizmosql_password: str -) -> "Generator[GizmoSQLService, None, None]": - """Run a second GizmoSQL container using the SQLite backend.""" - - def check(service: Any) -> bool: - try: - uri = f"grpc+tls://{service.host}:{service.port}" - db_kwargs = _gizmosql_db_kwargs(gizmosql_username, gizmosql_password) - with flightsql.connect(uri=uri, db_kwargs=db_kwargs, autocommit=True) as conn: - vendor_version = conn.adbc_get_info().get("vendor_version", "").lower() - return "sqlite" in vendor_version - except Exception: - return False - - worker_num = get_xdist_worker_num() - name = "gizmosql_sqlite" - if worker_num is not None: - name += f"_{worker_num}" - - env = { - "DATABASE_BACKEND": "sqlite", - "GIZMOSQL_PASSWORD": gizmosql_password, - "GIZMOSQL_USERNAME": gizmosql_username, - "TLS_ENABLED": "1", - } - - with docker_service.run( - image=gizmosql_image, - check=check, - container_port=31337, - name=name, - env=env, - timeout=90, - pause=1.0, - transient=True, - ) as service: - yield GizmoSQLService( - host=service.host, - port=service.port, - container=service.container, - username=gizmosql_username, - password=gizmosql_password, - ) - - -@pytest.fixture(scope="session") -def adbc_gizmosql_connection_config(gizmosql_service: "GizmoSQLService") -> "dict[str, Any]": +def adbc_gizmosql_connection_config(gizmosql_service: "GizmoSQLService") -> dict[str, Any]: """Shared connection configuration for DuckDB-backed GizmoSQL tests.""" - return _gizmosql_connection_config(gizmosql_service, backend="duckdb") @pytest.fixture(scope="session") -def adbc_gizmosql_config(adbc_gizmosql_connection_config: "dict[str, Any]") -> "Generator[AdbcConfig, None, None]": +def adbc_gizmosql_config(gizmosql_service: "GizmoSQLService") -> Generator[AdbcConfig, None, None]: """Provide an ADBC config targeting DuckDB-backed GizmoSQL.""" - - config = AdbcConfig(connection_config=dict(adbc_gizmosql_connection_config)) + config = _build_gizmosql_config(gizmosql_service, backend="duckdb") try: yield config finally: @@ -146,28 +69,23 @@ def adbc_gizmosql_config(adbc_gizmosql_connection_config: "dict[str, Any]") -> " @pytest.fixture(scope="function") -def adbc_gizmosql_session(adbc_gizmosql_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": +def adbc_gizmosql_session(adbc_gizmosql_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: """Create a DuckDB-backed GizmoSQL ADBC session.""" - with adbc_gizmosql_config.provide_session() as session: _prepare_gizmosql_test_table(session) yield session @pytest.fixture(scope="session") -def adbc_gizmosql_sqlite_connection_config(gizmosql_sqlite_service: "GizmoSQLService") -> "dict[str, Any]": +def adbc_gizmosql_sqlite_connection_config(gizmosql_sqlite_service: "GizmoSQLService") -> dict[str, Any]: """Shared connection configuration for SQLite-backed GizmoSQL tests.""" - return _gizmosql_connection_config(gizmosql_sqlite_service, backend="sqlite") @pytest.fixture(scope="session") -def adbc_gizmosql_sqlite_config( - adbc_gizmosql_sqlite_connection_config: "dict[str, Any]", -) -> "Generator[AdbcConfig, None, None]": +def adbc_gizmosql_sqlite_config(gizmosql_sqlite_service: "GizmoSQLService") -> Generator[AdbcConfig, None, None]: """Provide an ADBC config targeting SQLite-backed GizmoSQL.""" - - config = AdbcConfig(connection_config=dict(adbc_gizmosql_sqlite_connection_config)) + config = _build_gizmosql_config(gizmosql_sqlite_service, backend="sqlite") try: yield config finally: @@ -175,18 +93,16 @@ def adbc_gizmosql_sqlite_config( @pytest.fixture(scope="function") -def adbc_gizmosql_sqlite_session(adbc_gizmosql_sqlite_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": +def adbc_gizmosql_sqlite_session(adbc_gizmosql_sqlite_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: """Create a SQLite-backed GizmoSQL ADBC session.""" - with adbc_gizmosql_sqlite_config.provide_session() as session: _prepare_gizmosql_test_table(session) yield session @pytest.fixture(scope="session") -def adbc_postgresql_config(adbc_postgres_connection_config: "dict[str, str]") -> "Generator[AdbcConfig, None, None]": +def adbc_postgresql_config(adbc_postgres_connection_config: dict[str, str]) -> Generator[AdbcConfig, None, None]: """ADBC config using the PostgreSQL driver implementation.""" - connection_config = dict(adbc_postgres_connection_config) connection_config["driver_name"] = "adbc_driver_postgresql" config = AdbcConfig(connection_config=connection_config) @@ -197,9 +113,8 @@ def adbc_postgresql_config(adbc_postgres_connection_config: "dict[str, str]") -> @pytest.fixture(scope="function") -def adbc_postgresql_session(adbc_postgresql_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": +def adbc_postgresql_session(adbc_postgresql_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: """Create an ADBC PostgreSQL session with test table handling.""" - with adbc_postgresql_config.provide_session() as session: session.execute_script( """ @@ -215,7 +130,7 @@ def adbc_postgresql_session(adbc_postgresql_config: "AdbcConfig") -> "Generator[ yield session try: session.execute_script("DROP TABLE IF EXISTS test_table_adbc") - except Exception: # pragma: no cover - defensive cleanup + except Exception: try: session.execute("ROLLBACK") session.execute_script("DROP TABLE IF EXISTS test_table_adbc") @@ -224,9 +139,8 @@ def adbc_postgresql_session(adbc_postgresql_config: "AdbcConfig") -> "Generator[ @pytest.fixture(scope="function") -def adbc_sqlite_config() -> "Generator[AdbcConfig, None, None]": +def adbc_sqlite_config() -> Generator[AdbcConfig, None, None]: """ADBC configuration for SQLite tests.""" - config = AdbcConfig(connection_config={"uri": ":memory:", "driver_name": "adbc_driver_sqlite"}) try: yield config @@ -235,9 +149,8 @@ def adbc_sqlite_config() -> "Generator[AdbcConfig, None, None]": @pytest.fixture(scope="function") -def adbc_sqlite_session(adbc_sqlite_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": +def adbc_sqlite_session(adbc_sqlite_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: """Yield a SQLite-backed ADBC session.""" - with adbc_sqlite_config.provide_session() as session: session.execute_script( """ @@ -254,9 +167,8 @@ def adbc_sqlite_session(adbc_sqlite_config: "AdbcConfig") -> "Generator[AdbcDriv @pytest.fixture(scope="function") -def adbc_duckdb_config() -> "Generator[AdbcConfig, None, None]": +def adbc_duckdb_config() -> Generator[AdbcConfig, None, None]: """ADBC configuration for DuckDB tests.""" - config = AdbcConfig(connection_config={"driver_name": "adbc_driver_duckdb.dbapi.connect"}) try: yield config @@ -265,9 +177,8 @@ def adbc_duckdb_config() -> "Generator[AdbcConfig, None, None]": @pytest.fixture(scope="function") -def adbc_duckdb_session(adbc_duckdb_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": +def adbc_duckdb_session(adbc_duckdb_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: """Yield a DuckDB-backed ADBC session if the driver is available.""" - try: with adbc_duckdb_config.provide_session() as session: session.execute_script( @@ -294,7 +205,50 @@ def adbc_duckdb_session(adbc_duckdb_config: "AdbcConfig") -> "Generator[AdbcDriv @pytest.fixture(scope="function") -def adbc_duckdb_driver(adbc_duckdb_session: "AdbcDriver") -> "Generator[AdbcDriver, None, None]": +def adbc_duckdb_driver(adbc_duckdb_session: AdbcDriver) -> Generator[AdbcDriver, None, None]: """Alias fixture to emphasize driver usage in tests.""" - yield adbc_duckdb_session + + +def _require_gizmosql_driver() -> None: + """Skip when neither GizmoSQL nor FlightSQL ADBC driver package is installed.""" + if find_spec("adbc_driver_gizmosql") is None and find_spec("adbc_driver_flightsql") is None: + pytest.skip("adbc_driver_gizmosql or adbc_driver_flightsql required") + + +def _gizmosql_connection_config(service: "GizmoSQLService", *, backend: str) -> dict[str, Any]: + """Build SQLSpec GizmoSQL ADBC connection parameters from a GizmoSQLService.""" + _require_gizmosql_driver() + return { + "driver_name": "gizmosql", + "uri": service.uri, + "username": service.username, + "password": service.password, + "tls_skip_verify": True, + "gizmosql_backend": backend, + } + + +def _build_gizmosql_config(service: "GizmoSQLService", *, backend: str = "duckdb") -> AdbcConfig: + """Create an AdbcConfig bound to an upstream GizmoSQLService.""" + return AdbcConfig(connection_config=_gizmosql_connection_config(service, backend=backend)) + + +def _prepare_gizmosql_test_table(session: AdbcDriver) -> None: + """Reset the shared GizmoSQL ADBC scratch table before a test runs.""" + try: + session.execute("DROP TABLE IF EXISTS test_table_adbc") + except Exception: + pass + session.execute( + """ + CREATE TABLE test_table_adbc ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + value INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + session.execute("DELETE FROM test_table_adbc") + session.commit() diff --git a/tests/integration/fixtures/bigquery.py b/tests/integration/fixtures/bigquery.py index d125a7432..b982ce522 100644 --- a/tests/integration/fixtures/bigquery.py +++ b/tests/integration/fixtures/bigquery.py @@ -26,7 +26,7 @@ def _bigquery_connection_config(service: "BigQueryService", dataset_id: str | No return { "project": service.project, "dataset_id": dataset_id or f"`{service.project}`.`{service.dataset}`", - "client_options": ClientOptions(api_endpoint=f"http://{service.host}:{service.port}"), + "client_options": ClientOptions(api_endpoint=service.endpoint), "credentials": _anonymous_credentials(), } diff --git a/tests/integration/fixtures/spanner.py b/tests/integration/fixtures/spanner.py index 102abf16c..a430fca3e 100644 --- a/tests/integration/fixtures/spanner.py +++ b/tests/integration/fixtures/spanner.py @@ -5,7 +5,7 @@ import pytest from google.api_core import exceptions as api_exceptions -from google.cloud import spanner +from google.auth.credentials import AnonymousCredentials, Credentials from pytest_databases.docker.spanner import SpannerService from sqlspec import SQLSpec @@ -15,6 +15,9 @@ from google.cloud.spanner_v1.database import Database __all__ = ( + "build_spanner_connection_config", + "drop_table_if_exists", + "run_ddl", "spanner_async_config", "spanner_config", "spanner_database", @@ -24,40 +27,21 @@ ) -def build_spanner_connection_config(spanner_service: "SpannerService") -> "dict[str, Any]": +def build_spanner_connection_config(spanner_service: "SpannerService") -> dict[str, Any]: + """Build SQLSpec Spanner connection parameters targeting the Spanner emulator.""" return { "project": spanner_service.project, "instance_id": spanner_service.instance_name, "database_id": spanner_service.database_name, - "credentials": spanner_service.credentials, + "credentials": _anonymous_credentials(), "client_options": {"api_endpoint": f"{spanner_service.host}:{spanner_service.port}"}, "size": 5, } @pytest.fixture(scope="session") -def spanner_database( - spanner_service: "SpannerService", spanner_connection: "spanner.Client" -) -> "Generator[Database, None, None]": - """Ensure the emulator instance and database exist.""" - client = cast("Any", spanner_connection) - instance = client.instance(spanner_service.instance_name) - if not instance.exists(): - config_name = f"{spanner_connection.project_name}/instanceConfigs/emulator-config" - instance = client.instance(spanner_service.instance_name, configuration_name=config_name) - instance.create().result(300) - - database = instance.database(spanner_service.database_name) - if not database.exists(): - database.create().result(300) - yield database - - -@pytest.fixture(scope="session") -def spanner_config( - spanner_service: "SpannerService", spanner_connection: "spanner.Client", spanner_database: "Database" -) -> "Generator[SpannerSyncConfig, None, None]": - """Create a Spanner configuration after ensuring the database exists.""" +def spanner_config(spanner_service: "SpannerService") -> Generator[SpannerSyncConfig, None, None]: + """Create a session-scoped SpannerSyncConfig bound to the pre-bootstrapped emulator database.""" config = SpannerSyncConfig(connection_config=build_spanner_connection_config(spanner_service)) try: yield config @@ -65,9 +49,15 @@ def spanner_config( config.close_pool() +@pytest.fixture(scope="session") +def spanner_database(spanner_config: SpannerSyncConfig) -> "Database": + """Return the pre-bootstrapped Spanner emulator Database handle from SpannerSyncConfig.""" + return spanner_config.get_database() + + @pytest.fixture -def spanner_session(spanner_config: "SpannerSyncConfig") -> "Generator[SpannerSyncDriver, None, None]": - """Provide a read-only Spanner session.""" +def spanner_session(spanner_config: SpannerSyncConfig) -> Generator[SpannerSyncDriver, None, None]: + """Provide a Spanner session registered through SQLSpec.""" sql = SQLSpec() registered_config = sql.add_config(spanner_config) with sql.provide_session(registered_config) as session: @@ -75,25 +65,22 @@ def spanner_session(spanner_config: "SpannerSyncConfig") -> "Generator[SpannerSy @pytest.fixture -def spanner_write_session(spanner_config: "SpannerSyncConfig") -> "Generator[SpannerSyncDriver, None, None]": +def spanner_write_session(spanner_config: SpannerSyncConfig) -> Generator[SpannerSyncDriver, None, None]: """Provide a write-capable Spanner session.""" with spanner_config.provide_write_session() as session: yield session @pytest.fixture -def spanner_read_session(spanner_config: "SpannerSyncConfig") -> "Generator[SpannerSyncDriver, None, None]": +def spanner_read_session(spanner_config: SpannerSyncConfig) -> Generator[SpannerSyncDriver, None, None]: """Provide a read-only Spanner session.""" with spanner_config.provide_read_session() as session: yield session @pytest.fixture(scope="session") -async def spanner_async_config( - spanner_service: "SpannerService", spanner_database: "Database" -) -> "AsyncGenerator[SpannerAsyncConfig, None]": - """Create an async Spanner configuration after ensuring the database exists.""" - del spanner_database +async def spanner_async_config(spanner_service: "SpannerService") -> AsyncGenerator[SpannerAsyncConfig, None]: + """Create an async Spanner configuration bound to the pre-bootstrapped emulator database.""" config = SpannerAsyncConfig(connection_config=build_spanner_connection_config(spanner_service)) try: yield config @@ -101,7 +88,7 @@ async def spanner_async_config( await config.close_pool() -def run_ddl(database: "Database", statements: "list[str]", timeout: int = 300) -> None: +def run_ddl(database: "Database", statements: list[str], timeout: int = 300) -> None: """Execute DDL statements on a Spanner database.""" operation = cast("Any", database).update_ddl(statements) operation.result(timeout) @@ -113,3 +100,9 @@ def drop_table_if_exists(database: "Database", table_name: str) -> None: run_ddl(database, [f"DROP TABLE {table_name}"]) except api_exceptions.GoogleAPICallError: pass + + +def _anonymous_credentials() -> Credentials: + """Instantiate Google AnonymousCredentials for local Spanner emulator connections.""" + factory = cast("Any", AnonymousCredentials) + return cast("Credentials", factory()) From 665062e40e9d9fee9b449ad582a32a3116f6d4a2 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 16:24:13 +0000 Subject: [PATCH 03/35] test(fixtures): decouple cross-driver fixtures and guard optional adapter imports --- .../test_native_storage_capabilities.py | 16 +- .../extensions/adk/test_pgvector_migration.py | 100 +- .../sqlite/extensions/adk/test_maintenance.py | 25 +- .../extensions/adk/test_memory_store.py | 15 +- .../extensions/adk/test_owner_id_column.py | 15 +- .../sqlite/extensions/litestar/test_store.py | 28 +- .../test_load_from_arrow_transaction.py | 11 +- .../sqlite/sqlite/test_load_from_records.py | 2 + tests/integration/conftest.py | 5 +- tests/integration/fixtures/adapter_cases.py | 854 +++++++++++++----- tests/integration/fixtures/cockroach.py | 99 ++ tests/integration/fixtures/mssql.py | 179 ++-- tests/integration/fixtures/oracle.py | 111 ++- tests/integration/fixtures/postgres.py | 273 +++--- tests/integration/fixtures/sqlite.py | 31 +- 15 files changed, 1140 insertions(+), 624 deletions(-) create mode 100644 tests/integration/fixtures/cockroach.py diff --git a/tests/integration/adapters/cockroach/test_native_storage_capabilities.py b/tests/integration/adapters/cockroach/test_native_storage_capabilities.py index 2ee641a22..bda1e7fbc 100644 --- a/tests/integration/adapters/cockroach/test_native_storage_capabilities.py +++ b/tests/integration/adapters/cockroach/test_native_storage_capabilities.py @@ -1,6 +1,5 @@ """Local server contracts required by CockroachDB native object storage.""" -from collections.abc import Iterator from typing import TYPE_CHECKING, Literal from urllib.parse import urlencode, urlsplit, urlunsplit from uuid import uuid4 @@ -13,14 +12,15 @@ from tests.fixtures.rustfs import rustfs_filesystem if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Iterator + from pytest_databases import ContainerService from pytest_databases.docker.cockroachdb import CockroachDBService from pytest_databases.docker.rustfs import RustfsService @pytest.fixture -def native_connection(cockroachdb_service: "CockroachDBService") -> Iterator[psycopg.Connection]: +def native_connection(cockroachdb_service: "CockroachDBService") -> "Iterator[psycopg.Connection]": with psycopg.connect( host=cockroachdb_service.host, port=cockroachdb_service.port, @@ -33,10 +33,10 @@ def native_connection(cockroachdb_service: "CockroachDBService") -> Iterator[psy @pytest.fixture -def native_storage_uri(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> Iterator[str]: - rustfs_service.container.reload() - networks = rustfs_service.container.attrs["NetworkSettings"]["Networks"] - address = next(iter(networks.values()))["IPAddress"] +def native_storage_uri( + rustfs_service: "RustfsService", rustfs_bucket_name: str, docker_service: "ContainerService" +) -> "Iterator[str]": + address = docker_service.get_internal_network_ip(rustfs_service.container) prefix = f"native-{uuid4().hex}" query = urlencode({ "AWS_ACCESS_KEY_ID": rustfs_service.access_key, @@ -113,6 +113,7 @@ def test_native_csv_requires_explicit_null_encoding( def test_native_csv_marker_collides_with_literal_data( native_connection: psycopg.Connection, native_storage_uri: str, marker: str ) -> None: + """Verify an automatic CSV NULL marker collides with literal string data on roundtrip.""" files = native_connection.execute( "EXPORT INTO CSV %s WITH nullas = %s FROM " "(SELECT 1::INT8 AS id, NULL::STRING AS label UNION ALL SELECT 2, %s::STRING)", @@ -125,7 +126,6 @@ def test_native_csv_marker_collides_with_literal_data( sql.SQL("IMPORT INTO {} CSV DATA (%s) WITH nullif = %s").format(target), (_file_uri(native_storage_uri, files[0][0]), marker), ) - # An automatic marker would silently corrupt the second row. assert native_connection.execute(sql.SQL("SELECT * FROM {} ORDER BY id").format(target)).fetchall() == [ (1, None), (2, None), diff --git a/tests/integration/adapters/postgres/asyncpg/extensions/adk/test_pgvector_migration.py b/tests/integration/adapters/postgres/asyncpg/extensions/adk/test_pgvector_migration.py index 5e7c851eb..f80f3e9c5 100644 --- a/tests/integration/adapters/postgres/asyncpg/extensions/adk/test_pgvector_migration.py +++ b/tests/integration/adapters/postgres/asyncpg/extensions/adk/test_pgvector_migration.py @@ -2,18 +2,20 @@ import importlib import uuid -from collections.abc import AsyncGenerator from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import asyncpg import pytest -from pytest_databases.docker.postgres import PostgresService +from pytest_databases.docker.postgres import PostgresService, exec_psql from sqlspec.adapters.asyncpg import AsyncpgConfig from sqlspec.migrations.commands import AsyncMigrationCommands from sqlspec.migrations.context import MigrationContext +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + pytestmark = [pytest.mark.xdist_group("pgvector"), pytest.mark.asyncpg, pytest.mark.integration] migration = importlib.import_module("sqlspec.extensions.adk.migrations.0001_create_adk_tables") @@ -22,62 +24,31 @@ EMBEDDING_TYPE_SQL = ( "SELECT udt_name FROM information_schema.columns WHERE table_name = 'adk_memory' AND column_name = 'embedding'" ) -TERMINATE_SESSIONS_SQL = ( - "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()" -) - - -async def _connect(service: "PostgresService", database: str) -> "asyncpg.Connection[Any]": - return await asyncpg.connect( - host=service.host, port=service.port, user=service.user, password=service.password, database=database - ) - - -def _make_config(service: "PostgresService", database: str, migrations_path: "Path | None" = None) -> AsyncpgConfig: - migration_config: dict[str, Any] = {} - if migrations_path is not None: - migration_config = { - "script_location": str(migrations_path), - "include_extensions": ["adk"], - "version_table_name": "sqlspec_migrations_adk_pgvector", - } - return AsyncpgConfig( - connection_config={ - "host": service.host, - "port": service.port, - "user": service.user, - "password": service.password, - "database": database, - "min_size": 1, - "max_size": 2, - }, - migration_config=migration_config, - extension_config={"adk": {"enable_memory": True}}, - ) @pytest.fixture -async def fresh_database(pgvector_service: "PostgresService") -> "AsyncGenerator[str, None]": +async def fresh_database(pgvector_service: PostgresService) -> "AsyncGenerator[str, None]": """Create and drop a uniquely named database without the vector extension.""" database = f"adk_pgvector_{uuid.uuid4().hex}" - admin = await _connect(pgvector_service, pgvector_service.database) - try: - await admin.execute(f'CREATE DATABASE "{database}"') - finally: - await admin.close() + exit_code, output = exec_psql( + pgvector_service.container, + user=pgvector_service.user, + password=pgvector_service.password, + database="postgres", + sql=f'CREATE DATABASE "{database}"', + ) + if exit_code != 0: + decoded_output = output.decode("utf-8", errors="replace") + msg = f"Failed to create database {database!r}: {decoded_output}" + raise RuntimeError(msg) try: yield database finally: - admin = await _connect(pgvector_service, pgvector_service.database) - try: - await admin.execute(TERMINATE_SESSIONS_SQL, database) - await admin.execute(f'DROP DATABASE IF EXISTS "{database}"') - finally: - await admin.close() + pgvector_service.drop_database_force(database) async def test_memory_ddl_without_prerequisite_is_rejected( - pgvector_service: "PostgresService", fresh_database: str + pgvector_service: PostgresService, fresh_database: str ) -> None: """Dropping the leading extension statement makes the first vector column fail.""" context = MigrationContext(config=_make_config(pgvector_service, fresh_database), dialect="postgres") @@ -97,7 +68,7 @@ async def test_memory_ddl_without_prerequisite_is_rejected( async def test_packaged_migration_installs_vector_on_fresh_database( - pgvector_service: "PostgresService", fresh_database: str, tmp_path: Path + pgvector_service: PostgresService, fresh_database: str, tmp_path: Path ) -> None: """Packaged migration 0001 provisions vector and creates a usable embedding column.""" connection = await _connect(pgvector_service, fresh_database) @@ -123,7 +94,7 @@ async def test_packaged_migration_installs_vector_on_fresh_database( async def test_packaged_migration_reruns_when_vector_already_exists( - pgvector_service: "PostgresService", fresh_database: str, tmp_path: Path + pgvector_service: PostgresService, fresh_database: str, tmp_path: Path ) -> None: """Re-running migration 0001 against an existing vector installation succeeds.""" migrations_path = tmp_path / "migrations" @@ -143,3 +114,32 @@ async def test_packaged_migration_reruns_when_vector_already_exists( assert await connection.fetchval(EMBEDDING_TYPE_SQL) == "vector" finally: await connection.close() + + +async def _connect(service: PostgresService, database: str) -> "asyncpg.Connection[Any]": + return await asyncpg.connect( + host=service.host, port=service.port, user=service.user, password=service.password, database=database + ) + + +def _make_config(service: PostgresService, database: str, migrations_path: Path | None = None) -> AsyncpgConfig: + migration_config: dict[str, Any] = {} + if migrations_path is not None: + migration_config = { + "script_location": str(migrations_path), + "include_extensions": ["adk"], + "version_table_name": "sqlspec_migrations_adk_pgvector", + } + return AsyncpgConfig( + connection_config={ + "host": service.host, + "port": service.port, + "user": service.user, + "password": service.password, + "database": database, + "min_size": 1, + "max_size": 2, + }, + migration_config=migration_config, + extension_config={"adk": {"enable_memory": True}}, + ) diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py index ba57ae0d1..5cb77fabf 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py @@ -3,22 +3,37 @@ import tempfile from datetime import UTC, datetime, timedelta from pathlib import Path +from typing import TYPE_CHECKING from uuid import uuid4 import pytest from sqlspec.adapters.sqlite import SqliteConfig -from sqlspec.adapters.sqlite.adk import SqliteADKMemoryStore, SqliteADKStore -from sqlspec.adapters.sqlite.adk.store import _datetime_to_julian -from sqlspec.extensions.adk import StoredEvent, StoredMemory -from sqlspec.extensions.adk.maintenance import prune_events_sync, prune_memory_sync, prune_sessions_sync +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from sqlspec.adapters.sqlite.adk import SqliteADKMemoryStore, SqliteADKStore + from sqlspec.adapters.sqlite.adk.store import _datetime_to_julian + from sqlspec.extensions.adk import StoredEvent, StoredMemory + from sqlspec.extensions.adk.maintenance import prune_events_sync, prune_memory_sync, prune_sessions_sync +else: + SqliteADKMemoryStore = import_optional_attr("sqlspec.adapters.sqlite.adk", "SqliteADKMemoryStore") + SqliteADKStore = import_optional_attr("sqlspec.adapters.sqlite.adk", "SqliteADKStore") + _datetime_to_julian = import_optional_attr("sqlspec.adapters.sqlite.adk.store", "_datetime_to_julian") + StoredEvent = import_optional_attr("sqlspec.extensions.adk._types", "StoredEvent") + StoredMemory = import_optional_attr("sqlspec.extensions.adk.memory._types", "StoredMemory") + prune_events_sync = import_optional_attr("sqlspec.extensions.adk.maintenance", "prune_events_sync") + prune_memory_sync = import_optional_attr("sqlspec.extensions.adk.maintenance", "prune_memory_sync") + prune_sessions_sync = import_optional_attr("sqlspec.extensions.adk.maintenance", "prune_sessions_sync") + +pytest.importorskip("google.adk") pytestmark = pytest.mark.xdist_group("sqlite") def _build_memory_record( *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" -) -> StoredMemory: +) -> "StoredMemory": now = datetime.now(UTC) return StoredMemory( id=str(uuid4()), diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py index 018b4e1fb..bb5203599 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py @@ -2,20 +2,29 @@ import tempfile from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING from uuid import uuid4 import pytest from sqlspec.adapters.sqlite import SqliteConfig -from sqlspec.adapters.sqlite.adk import SqliteADKMemoryStore -from sqlspec.extensions.adk import StoredMemory +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from sqlspec.adapters.sqlite.adk import SqliteADKMemoryStore + from sqlspec.extensions.adk import StoredMemory +else: + SqliteADKMemoryStore = import_optional_attr("sqlspec.adapters.sqlite.adk", "SqliteADKMemoryStore") + StoredMemory = import_optional_attr("sqlspec.extensions.adk.memory._types", "StoredMemory") + +pytest.importorskip("google.adk") pytestmark = pytest.mark.xdist_group("sqlite") def _build_record( *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" -) -> StoredMemory: +) -> "StoredMemory": now = datetime.now(UTC) return StoredMemory( id=str(uuid4()), diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py index e7e9350dd..217e16bd5 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py @@ -6,12 +6,19 @@ import uuid from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from sqlspec.adapters.sqlite import SqliteConfig, SqliteConnectionParams -from sqlspec.adapters.sqlite.adk import SqliteADKStore +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from sqlspec.adapters.sqlite.adk import SqliteADKStore +else: + SqliteADKStore = import_optional_attr("sqlspec.adapters.sqlite.adk", "SqliteADKStore") + +pytest.importorskip("google.adk") pytestmark = [pytest.mark.xdist_group("sqlite"), pytest.mark.sqlite, pytest.mark.integration] @@ -340,8 +347,8 @@ def test_owner_id_column_ddl_extraction(sqlite_config: SqliteConfig) -> None: ) store = SqliteADKStore(config_with_extension) - assert store._owner_id_column_name == "tenant_id" # pyright: ignore[reportPrivateUsage] - assert store._owner_id_column_ddl == "tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE" # pyright: ignore[reportPrivateUsage] + assert store.owner_id_column_name == "tenant_id" + assert store.owner_id_column_ddl == "tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE" def test_create_session_without_fk_when_not_required( diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/litestar/test_store.py b/tests/integration/adapters/sqlite/sqlite/extensions/litestar/test_store.py index 9c99845f2..bb100e104 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/litestar/test_store.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/litestar/test_store.py @@ -7,20 +7,32 @@ """ import asyncio -from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING import pytest -from litestar.stores.base import Store -from litestar.stores.registry import StoreRegistry from sqlspec.adapters.sqlite.config import SqliteConfig -from sqlspec.adapters.sqlite.litestar.store import SQLiteStore +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + + from litestar.stores.base import Store + from litestar.stores.registry import StoreRegistry + + from sqlspec.adapters.sqlite.litestar.store import SQLiteStore +else: + Store = import_optional_attr("litestar.stores.base", "Store") + StoreRegistry = import_optional_attr("litestar.stores.registry", "StoreRegistry") + SQLiteStore = import_optional_attr("sqlspec.adapters.sqlite.litestar.store", "SQLiteStore") + +pytest.importorskip("litestar") pytestmark = [pytest.mark.sqlite, pytest.mark.integration, pytest.mark.xdist_group("sqlite")] @pytest.fixture -async def sqlite_store() -> AsyncGenerator[SQLiteStore, None]: +async def sqlite_store() -> "AsyncGenerator[SQLiteStore, None]": """Create SQLite store with shared in-memory database.""" config = SqliteConfig( connection_config={"database": "file:test_sessions_mem?mode=memory&cache=shared", "uri": True}, @@ -32,7 +44,7 @@ async def sqlite_store() -> AsyncGenerator[SQLiteStore, None]: await store.delete_all() -async def test_sync_to_thread_concurrency(sqlite_store: SQLiteStore) -> None: +async def test_sync_to_thread_concurrency(sqlite_store: "SQLiteStore") -> None: """Test concurrent access via sync_to_thread wrapper. SQLite has write serialization, so we test sequential writes @@ -41,7 +53,7 @@ async def test_sync_to_thread_concurrency(sqlite_store: SQLiteStore) -> None: for i in range(10): await sqlite_store.set(f"session_{i}", f"data_{i}".encode()) - async def read_session(session_id: int) -> "bytes | None": + async def read_session(session_id: int) -> bytes | None: return await sqlite_store.get(f"session_{session_id}") results = await asyncio.gather(*[read_session(i) for i in range(10)]) @@ -50,7 +62,7 @@ async def read_session(session_id: int) -> "bytes | None": assert result == f"data_{i}".encode() -async def test_store_implements_litestar_store_contract(sqlite_store: SQLiteStore) -> None: +async def test_store_implements_litestar_store_contract(sqlite_store: "SQLiteStore") -> None: """SQLite stores satisfy Litestar's nominal store and registry contract.""" registry = StoreRegistry() diff --git a/tests/integration/adapters/sqlite/sqlite/test_load_from_arrow_transaction.py b/tests/integration/adapters/sqlite/sqlite/test_load_from_arrow_transaction.py index 0f6303038..9755c2e9f 100644 --- a/tests/integration/adapters/sqlite/sqlite/test_load_from_arrow_transaction.py +++ b/tests/integration/adapters/sqlite/sqlite/test_load_from_arrow_transaction.py @@ -1,10 +1,19 @@ """SQLite load_from_arrow transactional atomicity.""" -import pyarrow as pa +from typing import TYPE_CHECKING + import pytest from sqlspec.adapters.sqlite import SqliteConfig from sqlspec.exceptions import SQLSpecError +from sqlspec.utils.module_loader import import_optional + +if TYPE_CHECKING: + import pyarrow as pa +else: + pa = import_optional("pyarrow") + +pytest.importorskip("pyarrow") def test_load_from_arrow_rolls_back_on_failure() -> None: diff --git a/tests/integration/adapters/sqlite/sqlite/test_load_from_records.py b/tests/integration/adapters/sqlite/sqlite/test_load_from_records.py index 4d61981da..3e4fafd65 100644 --- a/tests/integration/adapters/sqlite/sqlite/test_load_from_records.py +++ b/tests/integration/adapters/sqlite/sqlite/test_load_from_records.py @@ -5,6 +5,8 @@ from sqlspec.adapters.sqlite import SqliteConfig from sqlspec.exceptions import ImproperConfigurationError +pytest.importorskip("pyarrow") + def test_load_from_records_dict_and_positional() -> None: config = SqliteConfig(connection_config={"database": ":memory:"}) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d7cdd76a3..3184f1d7f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -48,7 +48,10 @@ def pytest_configure(config: pytest.Config) -> None: """Import integration fixture plugins whose backing driver packages are installed.""" - for module_info in sorted(pkgutil.iter_modules(fixtures_pkg.__path__), key=lambda item: item.name): + ordered_modules = sorted( + pkgutil.iter_modules(fixtures_pkg.__path__), key=lambda item: (item.name == "adapter_cases", item.name) + ) + for module_info in ordered_modules: if module_info.name in _NON_PLUGIN_FIXTURE_MODULES: continue requirements = _FIXTURE_DRIVER_REQUIREMENTS.get(module_info.name) diff --git a/tests/integration/fixtures/adapter_cases.py b/tests/integration/fixtures/adapter_cases.py index e2e0fb680..e2bb886af 100644 --- a/tests/integration/fixtures/adapter_cases.py +++ b/tests/integration/fixtures/adapter_cases.py @@ -1,10 +1,9 @@ """Fixtures for shared adapter contract tests.""" import contextlib -from collections.abc import AsyncGenerator, Callable, Generator from dataclasses import replace from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from uuid import uuid4 import pytest @@ -16,86 +15,8 @@ from pytest_databases.docker.postgres import PostgresService from pytest_databases.docker.spanner import SpannerService -from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver -from sqlspec.adapters.adbc.adk import AdbcADKStore -from sqlspec.adapters.adbc.litestar import ADBCStore -from sqlspec.adapters.aiomysql import AiomysqlConfig, AiomysqlDriver, AiomysqlDriverFeatures -from sqlspec.adapters.aiomysql.adk import AiomysqlADKStore -from sqlspec.adapters.aiomysql.litestar import AiomysqlStore -from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver, AiosqliteDriverFeatures -from sqlspec.adapters.aiosqlite.adk import AiosqliteADKStore -from sqlspec.adapters.aiosqlite.litestar import AiosqliteStore -from sqlspec.adapters.arrow_odbc import ArrowOdbcConfig, ArrowOdbcDriver -from sqlspec.adapters.arrow_odbc.adk import ArrowOdbcADKStore -from sqlspec.adapters.arrow_odbc.litestar import ArrowOdbcStore -from sqlspec.adapters.asyncmy import AsyncmyConfig, AsyncmyDriver, AsyncmyDriverFeatures -from sqlspec.adapters.asyncmy.adk import AsyncmyADKStore -from sqlspec.adapters.asyncmy.litestar import AsyncmyStore -from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver, AsyncpgDriverFeatures -from sqlspec.adapters.asyncpg.adk import AsyncpgADKStore -from sqlspec.adapters.asyncpg.litestar import AsyncpgStore -from sqlspec.adapters.bigquery import BigQueryConfig, BigQueryDriver, BigQueryDriverFeatures -from sqlspec.adapters.cockroach_asyncpg import ( - CockroachAsyncpgConfig, - CockroachAsyncpgDriver, - CockroachAsyncpgDriverFeatures, -) -from sqlspec.adapters.cockroach_asyncpg.adk import CockroachAsyncpgADKStore -from sqlspec.adapters.cockroach_psycopg import ( - CockroachPsycopgAsyncConfig, - CockroachPsycopgAsyncDriver, - CockroachPsycopgDriverFeatures, - CockroachPsycopgSyncConfig, - CockroachPsycopgSyncDriver, -) -from sqlspec.adapters.cockroach_psycopg.adk import CockroachPsycopgAsyncADKStore, CockroachPsycopgSyncADKStore -from sqlspec.adapters.db2 import Db2AsyncConfig, Db2AsyncDriver, Db2DriverFeatures, Db2SyncConfig, Db2SyncDriver -from sqlspec.adapters.db2.adk import Db2AsyncADKStore, Db2SyncADKStore -from sqlspec.adapters.db2.litestar import Db2AsyncStore, Db2SyncStore -from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBDriver, DuckDBDriverFeatures -from sqlspec.adapters.duckdb.adk import DuckdbADKStore -from sqlspec.adapters.duckdb.litestar import DuckdbStore -from sqlspec.adapters.mssql_python import MssqlPythonConfig, MssqlPythonDriver -from sqlspec.adapters.mssql_python.adk import MssqlPythonADKStore -from sqlspec.adapters.mysqlconnector import ( - MysqlConnectorAsyncConfig, - MysqlConnectorAsyncDriver, - MysqlConnectorDriverFeatures, - MysqlConnectorSyncConfig, - MysqlConnectorSyncDriver, -) -from sqlspec.adapters.mysqlconnector.adk import MysqlConnectorAsyncADKStore, MysqlConnectorSyncADKStore -from sqlspec.adapters.mysqlconnector.litestar import MysqlConnectorAsyncStore, MysqlConnectorSyncStore -from sqlspec.adapters.oracledb import ( - OracleAsyncConfig, - OracleAsyncDriver, - OracleDriverFeatures, - OracleSyncConfig, - OracleSyncDriver, -) -from sqlspec.adapters.oracledb.adk import OracleAsyncADKStore, OracleSyncADKStore -from sqlspec.adapters.oracledb.litestar import OracleAsyncStore, OracleSyncStore -from sqlspec.adapters.psqlpy import PsqlpyConfig, PsqlpyDriver, PsqlpyDriverFeatures, PsqlpyPoolParams -from sqlspec.adapters.psqlpy.adk import PsqlpyADKStore -from sqlspec.adapters.psqlpy.litestar import PsqlpyStore -from sqlspec.adapters.psycopg import ( - PsycopgAsyncConfig, - PsycopgAsyncDriver, - PsycopgDriverFeatures, - PsycopgSyncConfig, - PsycopgSyncDriver, -) -from sqlspec.adapters.psycopg.adk import PsycopgAsyncADKStore, PsycopgSyncADKStore -from sqlspec.adapters.psycopg.litestar import PsycopgAsyncStore, PsycopgSyncStore -from sqlspec.adapters.pymssql import PymssqlConfig, PymssqlDriver -from sqlspec.adapters.pymssql.adk import PymssqlADKStore -from sqlspec.adapters.pymysql import PyMysqlConfig, PyMysqlDriver, PyMysqlDriverFeatures -from sqlspec.adapters.pymysql.adk import PyMysqlADKStore -from sqlspec.adapters.pymysql.litestar import PyMysqlStore -from sqlspec.adapters.spanner import SpannerAsyncConfig, SpannerSyncConfig from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver, SqliteDriverFeatures -from sqlspec.adapters.sqlite.adk import SqliteADKStore -from sqlspec.adapters.sqlite.litestar import SQLiteStore +from sqlspec.utils.module_loader import import_optional_attr from tests.integration.adapters._shared._adk_cases import ADK_STORE_PARAMS, AdkStoreCase, AdkStoreCaseContext from tests.integration.adapters._shared._cases import ( ASYNC_DRIVER_PARAMS, @@ -136,28 +57,252 @@ build_bigquery_contract_table, ) from tests.integration.adapters._shared._store_cases import STORE_PARAMS, StoreCase, StoreCaseContext -from tests.integration.adapters.bigquery._wedge import describe_wedge, is_emulator_wedge -from tests.integration.fixtures.bigquery import _bigquery_connection_config +from tests.integration.fixtures.cockroach import _cockroach_asyncpg_connection_config, _cockroach_conninfo from tests.integration.fixtures.mssql import ( _arrow_odbc_connection_config, _mssql_connection_config, _mssql_python_connection_config, ) -from tests.integration.fixtures.mysql import _mysql_connection_config from tests.integration.fixtures.oracle import _oracle_pool_params from tests.integration.fixtures.postgres import ( _adbc_postgres_uri, - _cockroach_asyncpg_connection_config, - _cockroach_conninfo, _postgres_connection_config, _postgres_conninfo, _psqlpy_dsn, ) -from tests.integration.fixtures.spanner import build_spanner_connection_config + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable, Generator + + from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver + from sqlspec.adapters.adbc.adk import AdbcADKStore + from sqlspec.adapters.adbc.litestar import ADBCStore + from sqlspec.adapters.aiomysql import AiomysqlConfig, AiomysqlDriver, AiomysqlDriverFeatures + from sqlspec.adapters.aiomysql.adk import AiomysqlADKStore + from sqlspec.adapters.aiomysql.litestar import AiomysqlStore + from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver, AiosqliteDriverFeatures + from sqlspec.adapters.aiosqlite.adk import AiosqliteADKStore + from sqlspec.adapters.aiosqlite.litestar import AiosqliteStore + from sqlspec.adapters.arrow_odbc import ArrowOdbcConfig, ArrowOdbcDriver + from sqlspec.adapters.arrow_odbc.adk import ArrowOdbcADKStore + from sqlspec.adapters.arrow_odbc.litestar import ArrowOdbcStore + from sqlspec.adapters.asyncmy import AsyncmyConfig, AsyncmyDriver, AsyncmyDriverFeatures + from sqlspec.adapters.asyncmy.adk import AsyncmyADKStore + from sqlspec.adapters.asyncmy.litestar import AsyncmyStore + from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver, AsyncpgDriverFeatures + from sqlspec.adapters.asyncpg.adk import AsyncpgADKStore + from sqlspec.adapters.asyncpg.litestar import AsyncpgStore + from sqlspec.adapters.bigquery import BigQueryConfig, BigQueryDriver, BigQueryDriverFeatures + from sqlspec.adapters.cockroach_asyncpg import ( + CockroachAsyncpgConfig, + CockroachAsyncpgDriver, + CockroachAsyncpgDriverFeatures, + ) + from sqlspec.adapters.cockroach_asyncpg.adk import CockroachAsyncpgADKStore + from sqlspec.adapters.cockroach_psycopg import ( + CockroachPsycopgAsyncConfig, + CockroachPsycopgAsyncDriver, + CockroachPsycopgDriverFeatures, + CockroachPsycopgSyncConfig, + CockroachPsycopgSyncDriver, + ) + from sqlspec.adapters.cockroach_psycopg.adk import CockroachPsycopgAsyncADKStore, CockroachPsycopgSyncADKStore + from sqlspec.adapters.db2 import Db2AsyncConfig, Db2AsyncDriver, Db2DriverFeatures, Db2SyncConfig, Db2SyncDriver + from sqlspec.adapters.db2.adk import Db2AsyncADKStore, Db2SyncADKStore + from sqlspec.adapters.db2.litestar import Db2AsyncStore, Db2SyncStore + from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBDriver, DuckDBDriverFeatures + from sqlspec.adapters.duckdb.adk import DuckdbADKStore + from sqlspec.adapters.duckdb.litestar import DuckdbStore + from sqlspec.adapters.mssql_python import MssqlPythonConfig, MssqlPythonDriver + from sqlspec.adapters.mssql_python.adk import MssqlPythonADKStore + from sqlspec.adapters.mysqlconnector import ( + MysqlConnectorAsyncConfig, + MysqlConnectorAsyncDriver, + MysqlConnectorDriverFeatures, + MysqlConnectorSyncConfig, + MysqlConnectorSyncDriver, + ) + from sqlspec.adapters.mysqlconnector.adk import MysqlConnectorAsyncADKStore, MysqlConnectorSyncADKStore + from sqlspec.adapters.mysqlconnector.litestar import MysqlConnectorAsyncStore, MysqlConnectorSyncStore + from sqlspec.adapters.oracledb import ( + OracleAsyncConfig, + OracleAsyncDriver, + OracleDriverFeatures, + OracleSyncConfig, + OracleSyncDriver, + ) + from sqlspec.adapters.oracledb.adk import OracleAsyncADKStore, OracleSyncADKStore + from sqlspec.adapters.oracledb.litestar import OracleAsyncStore, OracleSyncStore + from sqlspec.adapters.psqlpy import PsqlpyConfig, PsqlpyDriver, PsqlpyDriverFeatures, PsqlpyPoolParams + from sqlspec.adapters.psqlpy.adk import PsqlpyADKStore + from sqlspec.adapters.psqlpy.litestar import PsqlpyStore + from sqlspec.adapters.psycopg import ( + PsycopgAsyncConfig, + PsycopgAsyncDriver, + PsycopgDriverFeatures, + PsycopgSyncConfig, + PsycopgSyncDriver, + ) + from sqlspec.adapters.psycopg.adk import PsycopgAsyncADKStore, PsycopgSyncADKStore + from sqlspec.adapters.psycopg.litestar import PsycopgAsyncStore, PsycopgSyncStore + from sqlspec.adapters.pymssql import PymssqlConfig, PymssqlDriver + from sqlspec.adapters.pymssql.adk import PymssqlADKStore + from sqlspec.adapters.pymysql import PyMysqlConfig, PyMysqlDriver, PyMysqlDriverFeatures + from sqlspec.adapters.pymysql.adk import PyMysqlADKStore + from sqlspec.adapters.pymysql.litestar import PyMysqlStore + from sqlspec.adapters.spanner import SpannerAsyncConfig, SpannerSyncConfig + from sqlspec.adapters.sqlite.adk import SqliteADKStore + from sqlspec.adapters.sqlite.litestar import SQLiteStore + from tests.integration.adapters.bigquery._wedge import describe_wedge, is_emulator_wedge + from tests.integration.fixtures.bigquery import _bigquery_connection_config + from tests.integration.fixtures.mysql import _mysql_connection_config + from tests.integration.fixtures.spanner import build_spanner_connection_config +else: + AdbcConfig = import_optional_attr("sqlspec.adapters.adbc", "AdbcConfig") + AdbcDriver = import_optional_attr("sqlspec.adapters.adbc", "AdbcDriver") + AdbcADKStore = import_optional_attr("sqlspec.adapters.adbc.adk", "AdbcADKStore") + ADBCStore = import_optional_attr("sqlspec.adapters.adbc.litestar", "ADBCStore") + AiomysqlConfig = import_optional_attr("sqlspec.adapters.aiomysql", "AiomysqlConfig") + AiomysqlDriver = import_optional_attr("sqlspec.adapters.aiomysql", "AiomysqlDriver") + AiomysqlDriverFeatures = import_optional_attr("sqlspec.adapters.aiomysql", "AiomysqlDriverFeatures") + AiomysqlADKStore = import_optional_attr("sqlspec.adapters.aiomysql.adk", "AiomysqlADKStore") + AiomysqlStore = import_optional_attr("sqlspec.adapters.aiomysql.litestar", "AiomysqlStore") + AiosqliteConfig = import_optional_attr("sqlspec.adapters.aiosqlite", "AiosqliteConfig") + AiosqliteDriver = import_optional_attr("sqlspec.adapters.aiosqlite", "AiosqliteDriver") + AiosqliteDriverFeatures = import_optional_attr("sqlspec.adapters.aiosqlite", "AiosqliteDriverFeatures") + AiosqliteADKStore = import_optional_attr("sqlspec.adapters.aiosqlite.adk", "AiosqliteADKStore") + AiosqliteStore = import_optional_attr("sqlspec.adapters.aiosqlite.litestar", "AiosqliteStore") + ArrowOdbcConfig = import_optional_attr("sqlspec.adapters.arrow_odbc", "ArrowOdbcConfig") + ArrowOdbcDriver = import_optional_attr("sqlspec.adapters.arrow_odbc", "ArrowOdbcDriver") + ArrowOdbcADKStore = import_optional_attr("sqlspec.adapters.arrow_odbc.adk", "ArrowOdbcADKStore") + ArrowOdbcStore = import_optional_attr("sqlspec.adapters.arrow_odbc.litestar", "ArrowOdbcStore") + AsyncmyConfig = import_optional_attr("sqlspec.adapters.asyncmy", "AsyncmyConfig") + AsyncmyDriver = import_optional_attr("sqlspec.adapters.asyncmy", "AsyncmyDriver") + AsyncmyDriverFeatures = import_optional_attr("sqlspec.adapters.asyncmy", "AsyncmyDriverFeatures") + AsyncmyADKStore = import_optional_attr("sqlspec.adapters.asyncmy.adk", "AsyncmyADKStore") + AsyncmyStore = import_optional_attr("sqlspec.adapters.asyncmy.litestar", "AsyncmyStore") + AsyncpgConfig = import_optional_attr("sqlspec.adapters.asyncpg", "AsyncpgConfig") + AsyncpgDriver = import_optional_attr("sqlspec.adapters.asyncpg", "AsyncpgDriver") + AsyncpgDriverFeatures = import_optional_attr("sqlspec.adapters.asyncpg", "AsyncpgDriverFeatures") + AsyncpgADKStore = import_optional_attr("sqlspec.adapters.asyncpg.adk", "AsyncpgADKStore") + AsyncpgStore = import_optional_attr("sqlspec.adapters.asyncpg.litestar", "AsyncpgStore") + BigQueryConfig = import_optional_attr("sqlspec.adapters.bigquery", "BigQueryConfig") + BigQueryDriver = import_optional_attr("sqlspec.adapters.bigquery", "BigQueryDriver") + BigQueryDriverFeatures = import_optional_attr("sqlspec.adapters.bigquery", "BigQueryDriverFeatures") + CockroachAsyncpgConfig = import_optional_attr("sqlspec.adapters.cockroach_asyncpg", "CockroachAsyncpgConfig") + CockroachAsyncpgDriver = import_optional_attr("sqlspec.adapters.cockroach_asyncpg", "CockroachAsyncpgDriver") + CockroachAsyncpgDriverFeatures = import_optional_attr( + "sqlspec.adapters.cockroach_asyncpg", "CockroachAsyncpgDriverFeatures" + ) + CockroachAsyncpgADKStore = import_optional_attr( + "sqlspec.adapters.cockroach_asyncpg.adk", "CockroachAsyncpgADKStore" + ) + CockroachPsycopgAsyncConfig = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgAsyncConfig" + ) + CockroachPsycopgAsyncDriver = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgAsyncDriver" + ) + CockroachPsycopgDriverFeatures = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgDriverFeatures" + ) + CockroachPsycopgSyncConfig = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgSyncConfig" + ) + CockroachPsycopgSyncDriver = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgSyncDriver" + ) + CockroachPsycopgAsyncADKStore = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg.adk", "CockroachPsycopgAsyncADKStore" + ) + CockroachPsycopgSyncADKStore = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg.adk", "CockroachPsycopgSyncADKStore" + ) + Db2AsyncConfig = import_optional_attr("sqlspec.adapters.db2", "Db2AsyncConfig") + Db2AsyncDriver = import_optional_attr("sqlspec.adapters.db2", "Db2AsyncDriver") + Db2DriverFeatures = import_optional_attr("sqlspec.adapters.db2", "Db2DriverFeatures") + Db2SyncConfig = import_optional_attr("sqlspec.adapters.db2", "Db2SyncConfig") + Db2SyncDriver = import_optional_attr("sqlspec.adapters.db2", "Db2SyncDriver") + Db2AsyncADKStore = import_optional_attr("sqlspec.adapters.db2.adk", "Db2AsyncADKStore") + Db2SyncADKStore = import_optional_attr("sqlspec.adapters.db2.adk", "Db2SyncADKStore") + Db2AsyncStore = import_optional_attr("sqlspec.adapters.db2.litestar", "Db2AsyncStore") + Db2SyncStore = import_optional_attr("sqlspec.adapters.db2.litestar", "Db2SyncStore") + DuckDBConfig = import_optional_attr("sqlspec.adapters.duckdb", "DuckDBConfig") + DuckDBDriver = import_optional_attr("sqlspec.adapters.duckdb", "DuckDBDriver") + DuckDBDriverFeatures = import_optional_attr("sqlspec.adapters.duckdb", "DuckDBDriverFeatures") + DuckdbADKStore = import_optional_attr("sqlspec.adapters.duckdb.adk", "DuckdbADKStore") + DuckdbStore = import_optional_attr("sqlspec.adapters.duckdb.litestar", "DuckdbStore") + MssqlPythonConfig = import_optional_attr("sqlspec.adapters.mssql_python", "MssqlPythonConfig") + MssqlPythonDriver = import_optional_attr("sqlspec.adapters.mssql_python", "MssqlPythonDriver") + MssqlPythonADKStore = import_optional_attr("sqlspec.adapters.mssql_python.adk", "MssqlPythonADKStore") + MysqlConnectorAsyncConfig = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorAsyncConfig") + MysqlConnectorAsyncDriver = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorAsyncDriver") + MysqlConnectorDriverFeatures = import_optional_attr( + "sqlspec.adapters.mysqlconnector", "MysqlConnectorDriverFeatures" + ) + MysqlConnectorSyncConfig = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorSyncConfig") + MysqlConnectorSyncDriver = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorSyncDriver") + MysqlConnectorAsyncADKStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.adk", "MysqlConnectorAsyncADKStore" + ) + MysqlConnectorSyncADKStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.adk", "MysqlConnectorSyncADKStore" + ) + MysqlConnectorAsyncStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.litestar", "MysqlConnectorAsyncStore" + ) + MysqlConnectorSyncStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.litestar", "MysqlConnectorSyncStore" + ) + OracleAsyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncConfig") + OracleAsyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncDriver") + OracleDriverFeatures = import_optional_attr("sqlspec.adapters.oracledb", "OracleDriverFeatures") + OracleSyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncConfig") + OracleSyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncDriver") + OracleAsyncADKStore = import_optional_attr("sqlspec.adapters.oracledb.adk", "OracleAsyncADKStore") + OracleSyncADKStore = import_optional_attr("sqlspec.adapters.oracledb.adk", "OracleSyncADKStore") + OracleAsyncStore = import_optional_attr("sqlspec.adapters.oracledb.litestar", "OracleAsyncStore") + OracleSyncStore = import_optional_attr("sqlspec.adapters.oracledb.litestar", "OracleSyncStore") + PsqlpyConfig = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyConfig") + PsqlpyDriver = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyDriver") + PsqlpyDriverFeatures = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyDriverFeatures") + PsqlpyPoolParams = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyPoolParams") + PsqlpyADKStore = import_optional_attr("sqlspec.adapters.psqlpy.adk", "PsqlpyADKStore") + PsqlpyStore = import_optional_attr("sqlspec.adapters.psqlpy.litestar", "PsqlpyStore") + PsycopgAsyncConfig = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgAsyncConfig") + PsycopgAsyncDriver = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgAsyncDriver") + PsycopgDriverFeatures = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgDriverFeatures") + PsycopgSyncConfig = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgSyncConfig") + PsycopgSyncDriver = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgSyncDriver") + PsycopgAsyncADKStore = import_optional_attr("sqlspec.adapters.psycopg.adk", "PsycopgAsyncADKStore") + PsycopgSyncADKStore = import_optional_attr("sqlspec.adapters.psycopg.adk", "PsycopgSyncADKStore") + PsycopgAsyncStore = import_optional_attr("sqlspec.adapters.psycopg.litestar", "PsycopgAsyncStore") + PsycopgSyncStore = import_optional_attr("sqlspec.adapters.psycopg.litestar", "PsycopgSyncStore") + PymssqlConfig = import_optional_attr("sqlspec.adapters.pymssql", "PymssqlConfig") + PymssqlDriver = import_optional_attr("sqlspec.adapters.pymssql", "PymssqlDriver") + PymssqlADKStore = import_optional_attr("sqlspec.adapters.pymssql.adk", "PymssqlADKStore") + PyMysqlConfig = import_optional_attr("sqlspec.adapters.pymysql", "PyMysqlConfig") + PyMysqlDriver = import_optional_attr("sqlspec.adapters.pymysql", "PyMysqlDriver") + PyMysqlDriverFeatures = import_optional_attr("sqlspec.adapters.pymysql", "PyMysqlDriverFeatures") + PyMysqlADKStore = import_optional_attr("sqlspec.adapters.pymysql.adk", "PyMysqlADKStore") + PyMysqlStore = import_optional_attr("sqlspec.adapters.pymysql.litestar", "PyMysqlStore") + SpannerAsyncConfig = import_optional_attr("sqlspec.adapters.spanner", "SpannerAsyncConfig") + SpannerSyncConfig = import_optional_attr("sqlspec.adapters.spanner", "SpannerSyncConfig") + SqliteADKStore = import_optional_attr("sqlspec.adapters.sqlite.adk", "SqliteADKStore") + SQLiteStore = import_optional_attr("sqlspec.adapters.sqlite.litestar", "SQLiteStore") + describe_wedge = import_optional_attr("tests.integration.adapters.bigquery._wedge", "describe_wedge") + is_emulator_wedge = import_optional_attr("tests.integration.adapters.bigquery._wedge", "is_emulator_wedge") + _bigquery_connection_config = import_optional_attr( + "tests.integration.fixtures.bigquery", "_bigquery_connection_config" + ) + _mysql_connection_config = import_optional_attr("tests.integration.fixtures.mysql", "_mysql_connection_config") + build_spanner_connection_config = import_optional_attr( + "tests.integration.fixtures.spanner", "build_spanner_connection_config" + ) @pytest.fixture -def contract_sqlite_driver(sqlite_config_regular_memory: SqliteConfig) -> Generator[SqliteDriver, None, None]: +def contract_sqlite_driver(sqlite_config_regular_memory: SqliteConfig) -> "Generator[SqliteDriver, None, None]": """Provide a fresh SQLite driver for contract tests.""" with sqlite_config_regular_memory.provide_session() as driver: driver.execute("PRAGMA foreign_keys = ON") @@ -167,8 +312,9 @@ def contract_sqlite_driver(sqlite_config_regular_memory: SqliteConfig) -> Genera @pytest.fixture -def contract_duckdb_driver(duckdb_basic_config: DuckDBConfig) -> Generator[DuckDBDriver, None, None]: +def contract_duckdb_driver(duckdb_basic_config: "DuckDBConfig") -> "Generator[DuckDBDriver, None, None]": """Provide a fresh DuckDB driver for contract tests.""" + pytest.importorskip("duckdb") with duckdb_basic_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(DUCKDB_CONTRACT_TABLE.create_sql) @@ -185,7 +331,9 @@ def contract_duckdb_driver(duckdb_basic_config: DuckDBConfig) -> Generator[DuckD @contextlib.contextmanager -def _provide_adbc_contract_driver(config: AdbcConfig, table: ContractTable) -> Generator[AdbcDriver, None, None]: +def _provide_adbc_contract_driver(config: "AdbcConfig", table: ContractTable) -> "Generator[AdbcDriver, None, None]": + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") try: with config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -199,22 +347,28 @@ def _provide_adbc_contract_driver(config: AdbcConfig, table: ContractTable) -> G @pytest.fixture -def contract_adbc_sqlite_driver(adbc_sqlite_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: +def contract_adbc_sqlite_driver(adbc_sqlite_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": """Provide a fresh ADBC SQLite driver for contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") with _provide_adbc_contract_driver(adbc_sqlite_config, DEFAULT_CONTRACT_TABLE) as driver: yield driver @pytest.fixture -def contract_adbc_duckdb_driver(adbc_duckdb_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: +def contract_adbc_duckdb_driver(adbc_duckdb_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": """Provide a fresh ADBC DuckDB driver for contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") with _provide_adbc_contract_driver(adbc_duckdb_config, DUCKDB_CONTRACT_TABLE) as driver: yield driver @pytest.fixture -def contract_adbc_postgres_driver(adbc_postgres_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: +def contract_adbc_postgres_driver(adbc_postgres_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": """Provide a fresh ADBC PostgreSQL driver for contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") with _provide_adbc_contract_driver(adbc_postgres_config, POSTGRES_CONTRACT_TABLE) as driver: yield driver @@ -234,6 +388,7 @@ def _bigquery_contract_session(bigquery_service: BigQueryService) -> "Generator[ config, so the client is reused across the xdist group, a dotted dataset_id keeps the default dataset unset, and tables are referenced fully-qualified. """ + pytest.importorskip("google.cloud.bigquery") config = BigQueryConfig( connection_config=_bigquery_connection_config(bigquery_service), driver_features={"job_result_timeout": 30.0, "job_retry_deadline": 0.0, "request_timeout": 15.0}, @@ -250,14 +405,15 @@ def _bigquery_contract_session(bigquery_service: BigQueryService) -> "Generator[ @pytest.fixture def contract_bigquery_driver( - _bigquery_contract_session: BigQueryDriver, bigquery_contract_table: ContractTable -) -> BigQueryDriver: + _bigquery_contract_session: "BigQueryDriver", bigquery_contract_table: ContractTable +) -> "BigQueryDriver": """Provide the session BigQuery driver with an isolated contract table per test. Once the emulator wedges (accepts requests but never responds), every call fails after the request timeout; skip the remaining BigQuery contract tests instead of paying that timeout per test. """ + pytest.importorskip("google.cloud.bigquery") global _bigquery_wedge_reason if _bigquery_wedge_reason is not None: pytest.skip(f"BigQuery emulator wedged earlier in this session ({_bigquery_wedge_reason})") @@ -272,8 +428,9 @@ def contract_bigquery_driver( @pytest.fixture -def contract_oracle_sync_driver(oracle_sync_config: OracleSyncConfig) -> Generator[OracleSyncDriver, None, None]: +def contract_oracle_sync_driver(oracle_sync_config: "OracleSyncConfig") -> "Generator[OracleSyncDriver, None, None]": """Provide a fresh Oracle sync driver for contract tests.""" + pytest.importorskip("oracledb") with oracle_sync_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(ORACLE_CONTRACT_TABLE.create_sql) @@ -287,9 +444,11 @@ def contract_oracle_sync_driver(oracle_sync_config: OracleSyncConfig) -> Generat @pytest.fixture def contract_arrow_odbc_mssql_driver( - arrow_odbc_mssql_config: ArrowOdbcConfig, -) -> Generator[ArrowOdbcDriver, None, None]: + arrow_odbc_mssql_config: "ArrowOdbcConfig", +) -> "Generator[ArrowOdbcDriver, None, None]": """Provide a fresh arrow-odbc driver backed by SQL Server.""" + pytest.importorskip("arrow_odbc") + pytest.importorskip("pyarrow") with arrow_odbc_mssql_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(MSSQL_CONTRACT_TABLE.create_sql) @@ -302,8 +461,11 @@ def contract_arrow_odbc_mssql_driver( @pytest.fixture -def contract_mssql_python_driver(mssql_python_config: MssqlPythonConfig) -> Generator[MssqlPythonDriver, None, None]: +def contract_mssql_python_driver( + mssql_python_config: "MssqlPythonConfig", +) -> "Generator[MssqlPythonDriver, None, None]": """Provide a fresh mssql-python driver backed by SQL Server.""" + pytest.importorskip("mssql_python") with mssql_python_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(MSSQL_CONTRACT_TABLE.create_sql) @@ -316,8 +478,9 @@ def contract_mssql_python_driver(mssql_python_config: MssqlPythonConfig) -> Gene @pytest.fixture -def contract_pymssql_driver(pymssql_config: PymssqlConfig) -> Generator[PymssqlDriver, None, None]: +def contract_pymssql_driver(pymssql_config: "PymssqlConfig") -> "Generator[PymssqlDriver, None, None]": """Provide a fresh pymssql driver backed by SQL Server.""" + pytest.importorskip("pymssql") with pymssql_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(MSSQL_CONTRACT_TABLE.create_sql) @@ -331,9 +494,10 @@ def contract_pymssql_driver(pymssql_config: PymssqlConfig) -> Generator[PymssqlD @pytest.fixture async def contract_oracle_async_driver( - oracle_async_config: OracleAsyncConfig, + oracle_async_config: "OracleAsyncConfig", ) -> "AsyncGenerator[OracleAsyncDriver, None]": """Provide a fresh Oracle async driver for contract tests.""" + pytest.importorskip("oracledb") async with oracle_async_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(ORACLE_CONTRACT_TABLE.create_sql) @@ -347,9 +511,10 @@ async def contract_oracle_async_driver( @pytest.fixture def contract_mysqlconnector_sync_driver( - mysqlconnector_sync_config: MysqlConnectorSyncConfig, -) -> Generator[MysqlConnectorSyncDriver, None, None]: + mysqlconnector_sync_config: "MysqlConnectorSyncConfig", +) -> "Generator[MysqlConnectorSyncDriver, None, None]": """Provide a fresh mysql-connector sync driver for contract tests.""" + pytest.importorskip("mysql.connector") with mysqlconnector_sync_config.provide_session() as driver: driver.execute("SET sql_notes = 0") driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -364,8 +529,9 @@ def contract_mysqlconnector_sync_driver( @pytest.fixture -def contract_pymysql_driver(pymysql_config: PyMysqlConfig) -> Generator[PyMysqlDriver, None, None]: +def contract_pymysql_driver(pymysql_config: "PyMysqlConfig") -> "Generator[PyMysqlDriver, None, None]": """Provide a fresh PyMySQL driver for contract tests.""" + pytest.importorskip("pymysql") with pymysql_config.provide_session() as driver: driver.execute("SET sql_notes = 0") driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -380,8 +546,12 @@ def contract_pymysql_driver(pymysql_config: PyMysqlConfig) -> Generator[PyMysqlD @pytest.fixture -def contract_psycopg_sync_driver(psycopg_sync_config: PsycopgSyncConfig) -> Generator[PsycopgSyncDriver, None, None]: +def contract_psycopg_sync_driver( + psycopg_sync_config: "PsycopgSyncConfig", +) -> "Generator[PsycopgSyncDriver, None, None]": """Provide a fresh psycopg sync driver for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") with psycopg_sync_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -395,9 +565,11 @@ def contract_psycopg_sync_driver(psycopg_sync_config: PsycopgSyncConfig) -> Gene @pytest.fixture def contract_cockroach_psycopg_sync_driver( - cockroach_psycopg_sync_config: CockroachPsycopgSyncConfig, -) -> Generator[CockroachPsycopgSyncDriver, None, None]: + cockroach_psycopg_sync_config: "CockroachPsycopgSyncConfig", +) -> "Generator[CockroachPsycopgSyncDriver, None, None]": """Provide a fresh CockroachDB psycopg sync driver for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") with cockroach_psycopg_sync_config.provide_session() as driver: driver.execute_script("DROP TABLE IF EXISTS contract_items") driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -410,8 +582,11 @@ def contract_cockroach_psycopg_sync_driver( @pytest.fixture -async def contract_aiosqlite_driver(aiosqlite_session_config: AiosqliteConfig) -> AsyncGenerator[AiosqliteDriver, None]: +async def contract_aiosqlite_driver( + aiosqlite_session_config: "AiosqliteConfig", +) -> "AsyncGenerator[AiosqliteDriver, None]": """Provide a fresh aiosqlite driver for contract tests.""" + pytest.importorskip("aiosqlite") async with aiosqlite_session_config.provide_session() as driver: await driver.execute("PRAGMA foreign_keys = ON") await driver.execute_script(DEFAULT_CONTRACT_TABLE.create_sql) @@ -420,8 +595,9 @@ async def contract_aiosqlite_driver(aiosqlite_session_config: AiosqliteConfig) - @pytest.fixture -async def contract_aiomysql_driver(aiomysql_config: AiomysqlConfig) -> AsyncGenerator[AiomysqlDriver, None]: +async def contract_aiomysql_driver(aiomysql_config: "AiomysqlConfig") -> "AsyncGenerator[AiomysqlDriver, None]": """Provide a fresh aiomysql driver for contract tests.""" + pytest.importorskip("aiomysql") async with aiomysql_config.provide_session() as driver: await driver.execute("SET sql_notes = 0") await driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -436,8 +612,9 @@ async def contract_aiomysql_driver(aiomysql_config: AiomysqlConfig) -> AsyncGene @pytest.fixture -async def contract_asyncmy_driver(asyncmy_config: AsyncmyConfig) -> AsyncGenerator[AsyncmyDriver, None]: +async def contract_asyncmy_driver(asyncmy_config: "AsyncmyConfig") -> "AsyncGenerator[AsyncmyDriver, None]": """Provide a fresh AsyncMy driver for contract tests.""" + pytest.importorskip("asyncmy") async with asyncmy_config.provide_session() as driver: await driver.execute("SET sql_notes = 0") await driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -453,9 +630,10 @@ async def contract_asyncmy_driver(asyncmy_config: AsyncmyConfig) -> AsyncGenerat @pytest.fixture async def contract_mysqlconnector_async_driver( - mysqlconnector_async_config: MysqlConnectorAsyncConfig, -) -> AsyncGenerator[MysqlConnectorAsyncDriver, None]: + mysqlconnector_async_config: "MysqlConnectorAsyncConfig", +) -> "AsyncGenerator[MysqlConnectorAsyncDriver, None]": """Provide a fresh mysql-connector async driver for contract tests.""" + pytest.importorskip("mysql.connector") async with mysqlconnector_async_config.provide_session() as driver: await driver.execute("SET sql_notes = 0") await driver.execute_script("DROP TABLE IF EXISTS contract_items") @@ -470,8 +648,9 @@ async def contract_mysqlconnector_async_driver( @pytest.fixture -async def contract_asyncpg_driver(asyncpg_config: AsyncpgConfig) -> AsyncGenerator[AsyncpgDriver, None]: +async def contract_asyncpg_driver(asyncpg_config: "AsyncpgConfig") -> "AsyncGenerator[AsyncpgDriver, None]": """Provide a fresh asyncpg driver for contract tests.""" + pytest.importorskip("asyncpg") async with asyncpg_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -484,8 +663,9 @@ async def contract_asyncpg_driver(asyncpg_config: AsyncpgConfig) -> AsyncGenerat @pytest.fixture -async def contract_psqlpy_driver(psqlpy_config: PsqlpyConfig) -> AsyncGenerator[PsqlpyDriver, None]: +async def contract_psqlpy_driver(psqlpy_config: "PsqlpyConfig") -> "AsyncGenerator[PsqlpyDriver, None]": """Provide a fresh psqlpy driver for contract tests.""" + pytest.importorskip("psqlpy") async with psqlpy_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -499,9 +679,11 @@ async def contract_psqlpy_driver(psqlpy_config: PsqlpyConfig) -> AsyncGenerator[ @pytest.fixture async def contract_psycopg_async_driver( - psycopg_async_config: PsycopgAsyncConfig, -) -> AsyncGenerator[PsycopgAsyncDriver, None]: + psycopg_async_config: "PsycopgAsyncConfig", +) -> "AsyncGenerator[PsycopgAsyncDriver, None]": """Provide a fresh psycopg async driver for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") async with psycopg_async_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -515,9 +697,10 @@ async def contract_psycopg_async_driver( @pytest.fixture async def contract_cockroach_asyncpg_driver( - cockroach_asyncpg_config: CockroachAsyncpgConfig, -) -> AsyncGenerator[CockroachAsyncpgDriver, None]: + cockroach_asyncpg_config: "CockroachAsyncpgConfig", +) -> "AsyncGenerator[CockroachAsyncpgDriver, None]": """Provide a fresh CockroachDB asyncpg driver for contract tests.""" + pytest.importorskip("asyncpg") async with cockroach_asyncpg_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -531,9 +714,11 @@ async def contract_cockroach_asyncpg_driver( @pytest.fixture async def contract_cockroach_psycopg_async_driver( - cockroach_psycopg_async_config: CockroachPsycopgAsyncConfig, -) -> AsyncGenerator[CockroachPsycopgAsyncDriver, None]: + cockroach_psycopg_async_config: "CockroachPsycopgAsyncConfig", +) -> "AsyncGenerator[CockroachPsycopgAsyncDriver, None]": """Provide a fresh CockroachDB psycopg async driver for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") async with cockroach_psycopg_async_config.provide_session() as driver: await driver.execute_script("DROP TABLE IF EXISTS contract_items") await driver.execute_script(POSTGRES_CONTRACT_TABLE.create_sql) @@ -556,7 +741,7 @@ def _events_migration_config(tmp_path: Path, suffix: str) -> dict[str, Any]: @pytest.fixture -def events_config_sqlite(tmp_path: Path) -> Callable[..., Any]: +def events_config_sqlite(tmp_path: Path) -> "Callable[..., Any]": """Build SQLite event-channel configs for contract tests.""" def make(*, extension_config: dict[str, Any], suffix: str) -> SqliteConfig: @@ -570,10 +755,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> SqliteConfig: @pytest.fixture -def events_config_duckdb(tmp_path: Path) -> Callable[..., Any]: +def events_config_duckdb(tmp_path: Path) -> "Callable[..., Any]": """Build DuckDB event-channel configs for contract tests.""" + pytest.importorskip("duckdb") - def make(*, extension_config: dict[str, Any], suffix: str) -> DuckDBConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "DuckDBConfig": return DuckDBConfig( connection_config={"database": str(tmp_path / f"events_{suffix}.duckdb")}, migration_config=_events_migration_config(tmp_path, suffix), @@ -584,10 +770,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> DuckDBConfig: @pytest.fixture -def events_config_aiosqlite(tmp_path: Path) -> Callable[..., Any]: +def events_config_aiosqlite(tmp_path: Path) -> "Callable[..., Any]": """Build aiosqlite event-channel configs for contract tests.""" + pytest.importorskip("aiosqlite") - def make(*, extension_config: dict[str, Any], suffix: str) -> AiosqliteConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "AiosqliteConfig": return AiosqliteConfig( connection_config={"database": str(tmp_path / f"events_{suffix}.db")}, migration_config=_events_migration_config(tmp_path, suffix), @@ -598,10 +785,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> AiosqliteConfig: @pytest.fixture -def events_config_pymysql(mysql_service: MySQLService, tmp_path: Path) -> Callable[..., Any]: +def events_config_pymysql(mysql_service: MySQLService, tmp_path: Path) -> "Callable[..., Any]": """Build PyMySQL event-channel configs for contract tests.""" + pytest.importorskip("pymysql") - def make(*, extension_config: dict[str, Any], suffix: str) -> PyMysqlConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "PyMysqlConfig": return PyMysqlConfig( connection_config=_mysql_connection_config(mysql_service), migration_config=_events_migration_config(tmp_path, suffix), @@ -612,10 +800,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> PyMysqlConfig: @pytest.fixture -def events_config_asyncmy(mysql_service: MySQLService, tmp_path: Path) -> Callable[..., Any]: +def events_config_asyncmy(mysql_service: MySQLService, tmp_path: Path) -> "Callable[..., Any]": """Build AsyncMy event-channel configs for contract tests.""" + pytest.importorskip("asyncmy") - def make(*, extension_config: dict[str, Any], suffix: str) -> AsyncmyConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "AsyncmyConfig": return AsyncmyConfig( connection_config=_mysql_connection_config(mysql_service), migration_config=_events_migration_config(tmp_path, suffix), @@ -626,10 +815,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> AsyncmyConfig: @pytest.fixture -def events_config_aiomysql(mysql_service: MySQLService, tmp_path: Path) -> Callable[..., Any]: +def events_config_aiomysql(mysql_service: MySQLService, tmp_path: Path) -> "Callable[..., Any]": """Build aiomysql event-channel configs for contract tests.""" + pytest.importorskip("aiomysql") - def make(*, extension_config: dict[str, Any], suffix: str) -> AiomysqlConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "AiomysqlConfig": return AiomysqlConfig( connection_config=_mysql_connection_config(mysql_service, database_key="db"), migration_config=_events_migration_config(tmp_path, suffix), @@ -640,10 +830,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> AiomysqlConfig: @pytest.fixture -def events_config_mysqlconnector_async(mysql_service: MySQLService, tmp_path: Path) -> Callable[..., Any]: +def events_config_mysqlconnector_async(mysql_service: MySQLService, tmp_path: Path) -> "Callable[..., Any]": """Build mysql-connector async event-channel configs for contract tests.""" + pytest.importorskip("mysql.connector") - def make(*, extension_config: dict[str, Any], suffix: str) -> MysqlConnectorAsyncConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "MysqlConnectorAsyncConfig": connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True return MysqlConnectorAsyncConfig( @@ -656,10 +847,12 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> MysqlConnectorAsyn @pytest.fixture -def events_config_psycopg_sync(postgres_service: PostgresService, tmp_path: Path) -> Callable[..., Any]: +def events_config_psycopg_sync(postgres_service: PostgresService, tmp_path: Path) -> "Callable[..., Any]": """Build psycopg sync event-channel configs for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") - def make(*, extension_config: dict[str, Any], suffix: str) -> PsycopgSyncConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "PsycopgSyncConfig": return PsycopgSyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, migration_config=_events_migration_config(tmp_path, suffix), @@ -670,10 +863,12 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> PsycopgSyncConfig: @pytest.fixture -def events_config_psycopg_async(postgres_service: PostgresService, tmp_path: Path) -> Callable[..., Any]: +def events_config_psycopg_async(postgres_service: PostgresService, tmp_path: Path) -> "Callable[..., Any]": """Build psycopg async event-channel configs for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") - def make(*, extension_config: dict[str, Any], suffix: str) -> PsycopgAsyncConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "PsycopgAsyncConfig": return PsycopgAsyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, migration_config=_events_migration_config(tmp_path, suffix), @@ -684,10 +879,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> PsycopgAsyncConfig @pytest.fixture -def events_config_psqlpy(postgres_service: PostgresService, tmp_path: Path) -> Callable[..., Any]: +def events_config_psqlpy(postgres_service: PostgresService, tmp_path: Path) -> "Callable[..., Any]": """Build psqlpy event-channel configs for contract tests.""" + pytest.importorskip("psqlpy") - def make(*, extension_config: dict[str, Any], suffix: str) -> PsqlpyConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "PsqlpyConfig": return PsqlpyConfig( connection_config={"dsn": _psqlpy_dsn(postgres_service)}, migration_config=_events_migration_config(tmp_path, suffix), @@ -698,10 +894,12 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> PsqlpyConfig: @pytest.fixture -def events_config_arrow_odbc_mssql(mssql_service: MSSQLService, tmp_path: Path) -> Callable[..., Any]: +def events_config_arrow_odbc_mssql(mssql_service: MSSQLService, tmp_path: Path) -> "Callable[..., Any]": """Build arrow-odbc SQL Server event-channel configs for contract tests.""" + pytest.importorskip("arrow_odbc") + pytest.importorskip("pyarrow") - def make(*, extension_config: dict[str, Any], suffix: str) -> ArrowOdbcConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "ArrowOdbcConfig": return ArrowOdbcConfig( connection_config=_arrow_odbc_connection_config(mssql_service), migration_config=_events_migration_config(tmp_path, suffix), @@ -713,10 +911,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> ArrowOdbcConfig: @pytest.fixture -def events_config_oracle_sync(oracle_23ai_service: OracleService, tmp_path: Path) -> Callable[..., Any]: +def events_config_oracle_sync(oracle_23ai_service: OracleService, tmp_path: Path) -> "Callable[..., Any]": """Build Oracle sync event-channel configs for contract tests.""" + pytest.importorskip("oracledb") - def make(*, extension_config: dict[str, Any], suffix: str) -> OracleSyncConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "OracleSyncConfig": return OracleSyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), migration_config=_events_migration_config(tmp_path, suffix), @@ -727,10 +926,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> OracleSyncConfig: @pytest.fixture -def events_config_oracle_async(oracle_23ai_service: OracleService, tmp_path: Path) -> Callable[..., Any]: +def events_config_oracle_async(oracle_23ai_service: OracleService, tmp_path: Path) -> "Callable[..., Any]": """Build Oracle async event-channel configs for contract tests.""" + pytest.importorskip("oracledb") - def make(*, extension_config: dict[str, Any], suffix: str) -> OracleAsyncConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "OracleAsyncConfig": return OracleAsyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), migration_config=_events_migration_config(tmp_path, suffix), @@ -741,10 +941,11 @@ def make(*, extension_config: dict[str, Any], suffix: str) -> OracleAsyncConfig: @pytest.fixture -def listen_notify_config_asyncpg(postgres_service: PostgresService) -> Callable[..., Any]: +def listen_notify_config_asyncpg(postgres_service: PostgresService) -> "Callable[..., Any]": """Build asyncpg native LISTEN/NOTIFY configs for contract tests.""" + pytest.importorskip("asyncpg") - def make(*, suffix: str) -> AsyncpgConfig: + def make(*, suffix: str) -> "AsyncpgConfig": return AsyncpgConfig( connection_config={"dsn": _postgres_conninfo(postgres_service)}, extension_config={"events": {"backend": "notify"}}, @@ -754,10 +955,11 @@ def make(*, suffix: str) -> AsyncpgConfig: @pytest.fixture -def listen_notify_config_psqlpy(postgres_service: PostgresService) -> Callable[..., Any]: +def listen_notify_config_psqlpy(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psqlpy native LISTEN/NOTIFY configs for contract tests.""" + pytest.importorskip("psqlpy") - def make(*, suffix: str) -> PsqlpyConfig: + def make(*, suffix: str) -> "PsqlpyConfig": return PsqlpyConfig( connection_config=PsqlpyPoolParams(dsn=_psqlpy_dsn(postgres_service)), extension_config={"events": {"backend": "notify"}}, @@ -767,10 +969,12 @@ def make(*, suffix: str) -> PsqlpyConfig: @pytest.fixture -def listen_notify_config_psycopg_sync(postgres_service: PostgresService) -> Callable[..., Any]: +def listen_notify_config_psycopg_sync(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psycopg sync native LISTEN/NOTIFY configs for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") - def make(*, suffix: str) -> PsycopgSyncConfig: + def make(*, suffix: str) -> "PsycopgSyncConfig": return PsycopgSyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, extension_config={"events": {"backend": "notify"}}, @@ -780,10 +984,12 @@ def make(*, suffix: str) -> PsycopgSyncConfig: @pytest.fixture -def listen_notify_config_psycopg_async(postgres_service: PostgresService) -> Callable[..., Any]: +def listen_notify_config_psycopg_async(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psycopg async native LISTEN/NOTIFY configs for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") - def make(*, suffix: str) -> PsycopgAsyncConfig: + def make(*, suffix: str) -> "PsycopgAsyncConfig": return PsycopgAsyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, extension_config={"events": {"backend": "notify"}}, @@ -863,6 +1069,7 @@ def make( @pytest.fixture def lifecycle_config_aiosqlite(tmp_path: Path) -> "Callable[..., AiosqliteConfig]": """Build fresh aiosqlite configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("aiosqlite") def make( *, @@ -870,7 +1077,7 @@ def make( driver_features: "AiosqliteDriverFeatures | None" = None, connection_overrides: "dict[str, Any] | None" = None, connection_instance: object | None = None, - ) -> AiosqliteConfig: + ) -> "AiosqliteConfig": connection_config = _lifecycle_connection_config( str(tmp_path / "lifecycle_aiosqlite.db"), pooled=pooled, connection_overrides=connection_overrides ) @@ -887,13 +1094,14 @@ def make( @pytest.fixture def lifecycle_config_duckdb(tmp_path: Path) -> "Callable[..., DuckDBConfig]": """Build fresh DuckDB configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("duckdb") def make( *, pooled: bool = False, driver_features: "DuckDBDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> DuckDBConfig: + ) -> "DuckDBConfig": connection_config = _lifecycle_connection_config(str(tmp_path / "lifecycle.duckdb"), pooled=pooled) extra: dict[str, Any] = {} if driver_features is not None: @@ -908,13 +1116,14 @@ def make( @pytest.fixture def lifecycle_config_asyncpg(postgres_service: PostgresService) -> "Callable[..., AsyncpgConfig]": """Build fresh asyncpg configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("asyncpg") def make( *, pooled: bool = False, driver_features: "AsyncpgDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> AsyncpgConfig: + ) -> "AsyncpgConfig": connection_config: dict[str, Any] = _postgres_connection_config(postgres_service) if pooled: connection_config.update({"min_size": 2, "max_size": 5}) @@ -931,13 +1140,14 @@ def make( @pytest.fixture def lifecycle_config_psqlpy(postgres_service: PostgresService) -> "Callable[..., PsqlpyConfig]": """Build fresh psqlpy configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("psqlpy") def make( *, pooled: bool = False, driver_features: "PsqlpyDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> PsqlpyConfig: + ) -> "PsqlpyConfig": connection_config: dict[str, Any] = {"dsn": _psqlpy_dsn(postgres_service)} if pooled: connection_config["max_db_pool_size"] = 5 @@ -954,6 +1164,8 @@ def make( @pytest.fixture def lifecycle_config_psycopg_sync(postgres_service: PostgresService) -> "Callable[..., PsycopgSyncConfig]": """Build fresh psycopg sync configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, @@ -961,7 +1173,7 @@ def make( driver_features: "PsycopgDriverFeatures | None" = None, connection_overrides: "dict[str, Any] | None" = None, connection_instance: object | None = None, - ) -> PsycopgSyncConfig: + ) -> "PsycopgSyncConfig": connection_config: dict[str, Any] = {"conninfo": _postgres_conninfo(postgres_service), "autocommit": True} if connection_overrides: connection_config.update(connection_overrides) @@ -980,6 +1192,8 @@ def make( @pytest.fixture def lifecycle_config_psycopg_async(postgres_service: PostgresService) -> "Callable[..., PsycopgAsyncConfig]": """Build fresh psycopg async configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, @@ -987,7 +1201,7 @@ def make( driver_features: "PsycopgDriverFeatures | None" = None, connection_overrides: "dict[str, Any] | None" = None, connection_instance: object | None = None, - ) -> PsycopgAsyncConfig: + ) -> "PsycopgAsyncConfig": connection_config: dict[str, Any] = {"conninfo": _postgres_conninfo(postgres_service), "autocommit": True} if connection_overrides: connection_config.update(connection_overrides) @@ -1008,13 +1222,14 @@ def lifecycle_config_cockroach_asyncpg( cockroachdb_service: CockroachDBService, ) -> "Callable[..., CockroachAsyncpgConfig]": """Build fresh CockroachDB asyncpg configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("asyncpg") def make( *, pooled: bool = False, driver_features: "CockroachAsyncpgDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> CockroachAsyncpgConfig: + ) -> "CockroachAsyncpgConfig": connection_config = _cockroach_asyncpg_connection_config(cockroachdb_service) if pooled: connection_config.update({"min_size": 2, "max_size": 5}) @@ -1033,13 +1248,15 @@ def lifecycle_config_cockroach_psycopg_sync( cockroachdb_service: CockroachDBService, ) -> "Callable[..., CockroachPsycopgSyncConfig]": """Build fresh CockroachDB psycopg sync configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, pooled: bool = False, driver_features: "CockroachPsycopgDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> CockroachPsycopgSyncConfig: + ) -> "CockroachPsycopgSyncConfig": connection_config: dict[str, Any] = {"conninfo": _cockroach_conninfo(cockroachdb_service)} if pooled: connection_config.update({"min_size": 2, "max_size": 5}) @@ -1058,13 +1275,15 @@ def lifecycle_config_cockroach_psycopg_async( cockroachdb_service: CockroachDBService, ) -> "Callable[..., CockroachPsycopgAsyncConfig]": """Build fresh CockroachDB psycopg async configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, pooled: bool = False, driver_features: "CockroachPsycopgDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> CockroachPsycopgAsyncConfig: + ) -> "CockroachPsycopgAsyncConfig": connection_config: dict[str, Any] = {"conninfo": _cockroach_conninfo(cockroachdb_service)} if pooled: connection_config.update({"min_size": 2, "max_size": 5}) @@ -1081,13 +1300,14 @@ def make( @pytest.fixture def lifecycle_config_aiomysql(mysql_service: MySQLService) -> "Callable[..., AiomysqlConfig]": """Build fresh aiomysql configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("aiomysql") def make( *, pooled: bool = False, driver_features: "AiomysqlDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> AiomysqlConfig: + ) -> "AiomysqlConfig": connection_config = _mysql_connection_config(mysql_service, database_key="db") if pooled: connection_config.update({"minsize": 2, "maxsize": 5}) @@ -1104,13 +1324,14 @@ def make( @pytest.fixture def lifecycle_config_asyncmy(mysql_service: MySQLService) -> "Callable[..., AsyncmyConfig]": """Build fresh asyncmy configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("asyncmy") def make( *, pooled: bool = False, driver_features: "AsyncmyDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> AsyncmyConfig: + ) -> "AsyncmyConfig": connection_config = _mysql_connection_config(mysql_service) if pooled: connection_config.update({"minsize": 2, "maxsize": 5}) @@ -1127,13 +1348,14 @@ def make( @pytest.fixture def lifecycle_config_mysqlconnector_sync(mysql_service: MySQLService) -> "Callable[..., MysqlConnectorSyncConfig]": """Build fresh mysql-connector sync configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("mysql.connector") def make( *, pooled: bool = False, driver_features: "MysqlConnectorDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> MysqlConnectorSyncConfig: + ) -> "MysqlConnectorSyncConfig": connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True if pooled: @@ -1151,10 +1373,11 @@ def make( @pytest.fixture def lifecycle_config_mysqlconnector_async(mysql_service: MySQLService) -> "Callable[..., MysqlConnectorAsyncConfig]": """Build fresh mysql-connector async configs for the connection-hook lifecycle contract (no pooling).""" + pytest.importorskip("mysql.connector") def make( *, pooled: bool = False, driver_features: "MysqlConnectorDriverFeatures | None" = None - ) -> MysqlConnectorAsyncConfig: + ) -> "MysqlConnectorAsyncConfig": connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True if driver_features is None: @@ -1167,13 +1390,14 @@ def make( @pytest.fixture def lifecycle_config_pymysql(mysql_service: MySQLService) -> "Callable[..., PyMysqlConfig]": """Build fresh PyMySQL configs for the pooling/connection-hook lifecycle contracts (thread-local pool).""" + pytest.importorskip("pymysql") def make( *, pooled: bool = False, driver_features: "PyMysqlDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> PyMysqlConfig: + ) -> "PyMysqlConfig": connection_config = _mysql_connection_config(mysql_service) extra: dict[str, Any] = {} if driver_features is not None: @@ -1188,8 +1412,9 @@ def make( @pytest.fixture def lifecycle_config_bigquery(bigquery_service: BigQueryService) -> "Callable[..., BigQueryConfig]": """Build fresh BigQuery configs for the connection-hook lifecycle contract (no pooling).""" + pytest.importorskip("google.cloud.bigquery") - def make(*, pooled: bool = False, driver_features: "BigQueryDriverFeatures | None" = None) -> BigQueryConfig: + def make(*, pooled: bool = False, driver_features: "BigQueryDriverFeatures | None" = None) -> "BigQueryConfig": connection_config = _bigquery_connection_config(bigquery_service) if driver_features is None: return BigQueryConfig(connection_config=connection_config) @@ -1201,13 +1426,14 @@ def make(*, pooled: bool = False, driver_features: "BigQueryDriverFeatures | Non @pytest.fixture def lifecycle_config_oracle_sync(oracle_23ai_service: OracleService) -> "Callable[..., OracleSyncConfig]": """Build fresh Oracle sync configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("oracledb") def make( *, pooled: bool = False, driver_features: "OracleDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> OracleSyncConfig: + ) -> "OracleSyncConfig": connection_config = _oracle_pool_params(oracle_23ai_service) extra: dict[str, Any] = {} if driver_features is not None: @@ -1222,13 +1448,14 @@ def make( @pytest.fixture def lifecycle_config_oracle_async(oracle_23ai_service: OracleService) -> "Callable[..., OracleAsyncConfig]": """Build fresh Oracle async configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("oracledb") def make( *, pooled: bool = False, driver_features: "OracleDriverFeatures | None" = None, connection_instance: object | None = None, - ) -> OracleAsyncConfig: + ) -> "OracleAsyncConfig": connection_config = _oracle_pool_params(oracle_23ai_service) extra: dict[str, Any] = {} if driver_features is not None: @@ -1241,7 +1468,7 @@ def make( @pytest.fixture -def migration_config_sqlite(tmp_path: Path) -> Callable[..., Any]: +def migration_config_sqlite(tmp_path: Path) -> "Callable[..., Any]": """Build SQLite configs for migration contract tests.""" def make(*, script_location: str, version_table_name: str, suffix: str) -> SqliteConfig: @@ -1254,8 +1481,9 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Sqlit @pytest.fixture -def migration_config_duckdb(tmp_path: Path) -> Callable[..., Any]: +def migration_config_duckdb(tmp_path: Path) -> "Callable[..., Any]": """Build DuckDB configs for migration contract tests.""" + pytest.importorskip("duckdb") def make( *, @@ -1264,7 +1492,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> DuckDBConfig: + ) -> "DuckDBConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1281,10 +1509,11 @@ def make( @pytest.fixture -def migration_config_aiosqlite(tmp_path: Path) -> Callable[..., Any]: +def migration_config_aiosqlite(tmp_path: Path) -> "Callable[..., Any]": """Build aiosqlite configs for migration contract tests.""" + pytest.importorskip("aiosqlite") - def make(*, script_location: str, version_table_name: str, suffix: str) -> AiosqliteConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "AiosqliteConfig": return AiosqliteConfig( connection_config={"database": str(tmp_path / f"mig_{suffix}.db")}, migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1294,10 +1523,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Aiosq @pytest.fixture -def migration_config_pymysql(mysql_service: MySQLService) -> Callable[..., Any]: +def migration_config_pymysql(mysql_service: MySQLService) -> "Callable[..., Any]": """Build PyMySQL configs for migration contract tests.""" + pytest.importorskip("pymysql") - def make(*, script_location: str, version_table_name: str, suffix: str) -> PyMysqlConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "PyMysqlConfig": return PyMysqlConfig( connection_config=_mysql_connection_config(mysql_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1307,10 +1537,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> PyMys @pytest.fixture -def migration_config_asyncmy(mysql_service: MySQLService) -> Callable[..., Any]: +def migration_config_asyncmy(mysql_service: MySQLService) -> "Callable[..., Any]": """Build AsyncMy configs for migration contract tests.""" + pytest.importorskip("asyncmy") - def make(*, script_location: str, version_table_name: str, suffix: str) -> AsyncmyConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "AsyncmyConfig": return AsyncmyConfig( connection_config=_mysql_connection_config(mysql_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1320,10 +1551,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Async @pytest.fixture -def migration_config_aiomysql(mysql_service: MySQLService) -> Callable[..., Any]: +def migration_config_aiomysql(mysql_service: MySQLService) -> "Callable[..., Any]": """Build aiomysql configs for migration contract tests.""" + pytest.importorskip("aiomysql") - def make(*, script_location: str, version_table_name: str, suffix: str) -> AiomysqlConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "AiomysqlConfig": return AiomysqlConfig( connection_config=_mysql_connection_config(mysql_service, database_key="db"), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1333,10 +1565,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Aiomy @pytest.fixture -def migration_config_mysqlconnector_async(mysql_service: MySQLService) -> Callable[..., Any]: +def migration_config_mysqlconnector_async(mysql_service: MySQLService) -> "Callable[..., Any]": """Build mysql-connector async configs for migration contract tests.""" + pytest.importorskip("mysql.connector") - def make(*, script_location: str, version_table_name: str, suffix: str) -> MysqlConnectorAsyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "MysqlConnectorAsyncConfig": connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True return MysqlConnectorAsyncConfig( @@ -1348,8 +1581,9 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Mysql @pytest.fixture -def migration_config_asyncpg(postgres_service: PostgresService) -> Callable[..., Any]: +def migration_config_asyncpg(postgres_service: PostgresService) -> "Callable[..., Any]": """Build asyncpg configs for migration contract tests.""" + pytest.importorskip("asyncpg") def make( *, @@ -1358,7 +1592,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> AsyncpgConfig: + ) -> "AsyncpgConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1375,8 +1609,10 @@ def make( @pytest.fixture -def migration_config_psycopg_sync(postgres_service: PostgresService) -> Callable[..., Any]: +def migration_config_psycopg_sync(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psycopg sync configs for migration contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, @@ -1385,7 +1621,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> PsycopgSyncConfig: + ) -> "PsycopgSyncConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1402,8 +1638,10 @@ def make( @pytest.fixture -def migration_config_psycopg_async(postgres_service: PostgresService) -> Callable[..., Any]: +def migration_config_psycopg_async(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psycopg async configs for migration contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") def make( *, @@ -1412,7 +1650,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> PsycopgAsyncConfig: + ) -> "PsycopgAsyncConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1429,8 +1667,9 @@ def make( @pytest.fixture -def migration_config_psqlpy(postgres_service: PostgresService) -> Callable[..., Any]: +def migration_config_psqlpy(postgres_service: PostgresService) -> "Callable[..., Any]": """Build psqlpy configs for migration contract tests.""" + pytest.importorskip("psqlpy") def make( *, @@ -1439,7 +1678,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> PsqlpyConfig: + ) -> "PsqlpyConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1454,8 +1693,10 @@ def make( @pytest.fixture -def migration_config_adbc_postgres(postgres_service: PostgresService) -> Callable[..., Any]: +def migration_config_adbc_postgres(postgres_service: PostgresService) -> "Callable[..., Any]": """Build ADBC PostgreSQL configs for migration contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") def make( *, @@ -1464,7 +1705,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> AdbcConfig: + ) -> "AdbcConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1482,10 +1723,12 @@ def make( @pytest.fixture -def migration_config_adbc_sqlite(tmp_path: Path) -> Callable[..., Any]: +def migration_config_adbc_sqlite(tmp_path: Path) -> "Callable[..., Any]": """Build ADBC SQLite configs for migration contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") - def make(*, script_location: str, version_table_name: str, suffix: str) -> AdbcConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "AdbcConfig": return AdbcConfig( connection_config={ "uri": f"file:{tmp_path / f'mig_{suffix}.db'}", @@ -1499,10 +1742,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> AdbcC @pytest.fixture -def migration_config_oracle_sync(oracle_23ai_service: OracleService) -> Callable[..., Any]: +def migration_config_oracle_sync(oracle_23ai_service: OracleService) -> "Callable[..., Any]": """Build Oracle sync configs for migration contract tests.""" + pytest.importorskip("oracledb") - def make(*, script_location: str, version_table_name: str, suffix: str) -> OracleSyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "OracleSyncConfig": return OracleSyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1512,10 +1756,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Oracl @pytest.fixture -def migration_config_oracle_async(oracle_23ai_service: OracleService) -> Callable[..., Any]: +def migration_config_oracle_async(oracle_23ai_service: OracleService) -> "Callable[..., Any]": """Build Oracle async configs for migration contract tests.""" + pytest.importorskip("oracledb") - def make(*, script_location: str, version_table_name: str, suffix: str) -> OracleAsyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "OracleAsyncConfig": return OracleAsyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -1525,8 +1770,9 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Oracl @pytest.fixture -def migration_config_mssql_python(mssql_python_migration_connection_config: dict[str, Any]) -> Callable[..., Any]: +def migration_config_mssql_python(mssql_python_migration_connection_config: dict[str, Any]) -> "Callable[..., Any]": """Build mssql-python sync configs for migration contract tests.""" + pytest.importorskip("mssql_python") def make( *, @@ -1535,7 +1781,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> MssqlPythonConfig: + ) -> "MssqlPythonConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1552,8 +1798,9 @@ def make( @pytest.fixture -def migration_config_pymssql(mssql_migration_connection_config: dict[str, Any]) -> Callable[..., Any]: +def migration_config_pymssql(mssql_migration_connection_config: dict[str, Any]) -> "Callable[..., Any]": """Build pymssql sync configs for migration contract tests.""" + pytest.importorskip("pymssql") def make( *, @@ -1562,7 +1809,7 @@ def make( suffix: str, default_schema: str | None = None, version_table_schema: str | None = None, - ) -> PymssqlConfig: + ) -> "PymssqlConfig": migration_config: dict[str, Any] = { "script_location": script_location, "version_table_name": version_table_name, @@ -1601,6 +1848,7 @@ def async_migration_case(request: pytest.FixtureRequest) -> MigrationCaseContext @pytest.fixture async def contract_sqlite_store() -> "AsyncGenerator[SQLiteStore, None]": """Provide a ready SQLite Litestar store for contract tests.""" + pytest.importorskip("litestar") config = SqliteConfig( connection_config={"database": "file:contract_store_sqlite?mode=memory&cache=shared", "uri": True}, extension_config=_STORE_EXTENSION_CONFIG, @@ -1616,6 +1864,8 @@ async def contract_sqlite_store() -> "AsyncGenerator[SQLiteStore, None]": @pytest.fixture async def contract_aiosqlite_store() -> "AsyncGenerator[AiosqliteStore, None]": """Provide a ready aiosqlite Litestar store for contract tests.""" + pytest.importorskip("aiosqlite") + pytest.importorskip("litestar") config = AiosqliteConfig(connection_config={"database": ":memory:"}, extension_config=_STORE_EXTENSION_CONFIG) store = AiosqliteStore(config) await store.create_table() @@ -1628,6 +1878,8 @@ async def contract_aiosqlite_store() -> "AsyncGenerator[AiosqliteStore, None]": @pytest.fixture async def contract_duckdb_store(tmp_path: Path) -> "AsyncGenerator[DuckdbStore, None]": """Provide a ready DuckDB Litestar store for contract tests.""" + pytest.importorskip("duckdb") + pytest.importorskip("litestar") config = DuckDBConfig( connection_config={"database": str(tmp_path / f"store_{uuid4().hex}.duckdb")}, extension_config=_STORE_EXTENSION_CONFIG, @@ -1643,6 +1895,8 @@ async def contract_duckdb_store(tmp_path: Path) -> "AsyncGenerator[DuckdbStore, @pytest.fixture async def contract_asyncpg_store(postgres_service: PostgresService) -> "AsyncGenerator[AsyncpgStore, None]": """Provide a ready asyncpg Litestar store for contract tests.""" + pytest.importorskip("asyncpg") + pytest.importorskip("litestar") config = AsyncpgConfig( connection_config=_postgres_connection_config(postgres_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -1657,6 +1911,8 @@ async def contract_asyncpg_store(postgres_service: PostgresService) -> "AsyncGen @pytest.fixture async def contract_psqlpy_store(postgres_service: PostgresService) -> "AsyncGenerator[PsqlpyStore, None]": """Provide a ready psqlpy Litestar store for contract tests.""" + pytest.importorskip("psqlpy") + pytest.importorskip("litestar") config = PsqlpyConfig( connection_config={"dsn": _psqlpy_dsn(postgres_service)}, extension_config=_STORE_EXTENSION_CONFIG ) @@ -1671,6 +1927,9 @@ async def contract_psqlpy_store(postgres_service: PostgresService) -> "AsyncGene @pytest.fixture async def contract_psycopg_async_store(postgres_service: PostgresService) -> "AsyncGenerator[PsycopgAsyncStore, None]": """Provide a ready psycopg async Litestar store for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("litestar") config = PsycopgAsyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, extension_config=_STORE_EXTENSION_CONFIG ) @@ -1685,6 +1944,9 @@ async def contract_psycopg_async_store(postgres_service: PostgresService) -> "As @pytest.fixture async def contract_psycopg_sync_store(postgres_service: PostgresService) -> "AsyncGenerator[PsycopgSyncStore, None]": """Provide a ready psycopg sync Litestar store for contract tests.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("litestar") config = PsycopgSyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service)}, extension_config=_STORE_EXTENSION_CONFIG ) @@ -1699,6 +1961,8 @@ async def contract_psycopg_sync_store(postgres_service: PostgresService) -> "Asy @pytest.fixture async def contract_aiomysql_store(mysql_service: MySQLService) -> "AsyncGenerator[AiomysqlStore, None]": """Provide a ready aiomysql Litestar store for contract tests.""" + pytest.importorskip("aiomysql") + pytest.importorskip("litestar") config = AiomysqlConfig( connection_config=_mysql_connection_config(mysql_service, database_key="db"), extension_config=_STORE_EXTENSION_CONFIG, @@ -1714,6 +1978,8 @@ async def contract_aiomysql_store(mysql_service: MySQLService) -> "AsyncGenerato @pytest.fixture async def contract_asyncmy_store(mysql_service: MySQLService) -> "AsyncGenerator[AsyncmyStore, None]": """Provide a ready asyncmy Litestar store for contract tests.""" + pytest.importorskip("asyncmy") + pytest.importorskip("litestar") config = AsyncmyConfig( connection_config=_mysql_connection_config(mysql_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -1730,6 +1996,8 @@ async def contract_mysqlconnector_async_store( mysql_service: MySQLService, ) -> "AsyncGenerator[MysqlConnectorAsyncStore, None]": """Provide a ready mysql-connector async Litestar store for contract tests.""" + pytest.importorskip("mysql.connector") + pytest.importorskip("litestar") connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True config = MysqlConnectorAsyncConfig(connection_config=connection_config, extension_config=_STORE_EXTENSION_CONFIG) @@ -1746,6 +2014,8 @@ async def contract_mysqlconnector_sync_store( mysql_service: MySQLService, ) -> "AsyncGenerator[MysqlConnectorSyncStore, None]": """Provide a ready mysql-connector sync Litestar store for contract tests.""" + pytest.importorskip("mysql.connector") + pytest.importorskip("litestar") connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True config = MysqlConnectorSyncConfig(connection_config=connection_config, extension_config=_STORE_EXTENSION_CONFIG) @@ -1760,6 +2030,8 @@ async def contract_mysqlconnector_sync_store( @pytest.fixture async def contract_pymysql_store(mysql_service: MySQLService) -> "AsyncGenerator[PyMysqlStore, None]": """Provide a ready PyMySQL Litestar store for contract tests.""" + pytest.importorskip("pymysql") + pytest.importorskip("litestar") config = PyMysqlConfig( connection_config=_mysql_connection_config(mysql_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -1774,6 +2046,9 @@ async def contract_pymysql_store(mysql_service: MySQLService) -> "AsyncGenerator @pytest.fixture async def contract_arrow_odbc_store(mssql_service: MSSQLService) -> "AsyncGenerator[ArrowOdbcStore, None]": """Provide a ready arrow-odbc SQL Server Litestar store for contract tests.""" + pytest.importorskip("arrow_odbc") + pytest.importorskip("pyarrow") + pytest.importorskip("litestar") config = ArrowOdbcConfig( connection_config=_arrow_odbc_connection_config(mssql_service), extension_config=_STORE_EXTENSION_CONFIG, @@ -1804,6 +2079,9 @@ async def contract_arrow_odbc_store(mssql_service: MSSQLService) -> "AsyncGenera @pytest.fixture async def contract_adbc_store(postgres_service: PostgresService) -> "AsyncGenerator[ADBCStore, None]": """Provide a ready PostgreSQL-backed ADBC Litestar store for contract tests.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") + pytest.importorskip("litestar") config = AdbcConfig( connection_config={"uri": _adbc_postgres_uri(postgres_service), "driver_name": "adbc_driver_postgresql"}, extension_config=_STORE_EXTENSION_CONFIG, @@ -1819,6 +2097,8 @@ async def contract_adbc_store(postgres_service: PostgresService) -> "AsyncGenera @pytest.fixture async def contract_oracle_async_store(oracle_23ai_service: OracleService) -> "AsyncGenerator[OracleAsyncStore, None]": """Provide a ready Oracle async Litestar store for contract tests.""" + pytest.importorskip("oracledb") + pytest.importorskip("litestar") config = OracleAsyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -1834,6 +2114,8 @@ async def contract_oracle_async_store(oracle_23ai_service: OracleService) -> "As @pytest.fixture async def contract_oracle_sync_store(oracle_23ai_service: OracleService) -> "AsyncGenerator[OracleSyncStore, None]": """Provide a ready Oracle sync Litestar store for contract tests.""" + pytest.importorskip("oracledb") + pytest.importorskip("litestar") config = OracleSyncConfig( connection_config=_oracle_pool_params(oracle_23ai_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -1858,7 +2140,7 @@ def _adk_extension_config(suffix: str) -> dict[str, Any]: } -def _ensure_adbc_store_driver_available(config: AdbcConfig) -> None: +def _ensure_adbc_store_driver_available(config: "AdbcConfig") -> None: try: with config.provide_session(): pass @@ -1869,8 +2151,9 @@ def _ensure_adbc_store_driver_available(config: AdbcConfig) -> None: @pytest.fixture -def adk_store_sqlite(tmp_path: Path) -> Callable[..., Any]: +def adk_store_sqlite(tmp_path: Path) -> "Callable[..., Any]": """Build a fresh SQLite ADK store with isolated tables per call.""" + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1884,8 +2167,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_aiosqlite(tmp_path: Path) -> Callable[..., Any]: +def adk_store_aiosqlite(tmp_path: Path) -> "Callable[..., Any]": """Build a fresh aiosqlite ADK store with isolated tables per call.""" + pytest.importorskip("aiosqlite") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1899,8 +2184,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_duckdb(tmp_path: Path) -> Callable[..., Any]: +def adk_store_duckdb(tmp_path: Path) -> "Callable[..., Any]": """Build a fresh DuckDB ADK store with isolated tables per call.""" + pytest.importorskip("duckdb") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1914,8 +2201,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_aiomysql(mysql_service: MySQLService) -> Callable[..., Any]: +def adk_store_aiomysql(mysql_service: MySQLService) -> "Callable[..., Any]": """Build a fresh aiomysql ADK store with isolated tables per call.""" + pytest.importorskip("aiomysql") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1929,8 +2218,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_asyncmy(mysql_service: MySQLService) -> Callable[..., Any]: +def adk_store_asyncmy(mysql_service: MySQLService) -> "Callable[..., Any]": """Build a fresh asyncmy ADK store with isolated tables per call.""" + pytest.importorskip("asyncmy") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1943,8 +2234,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_mysqlconnector_async(mysql_service: MySQLService) -> Callable[..., Any]: +def adk_store_mysqlconnector_async(mysql_service: MySQLService) -> "Callable[..., Any]": """Build a fresh mysql-connector async ADK store with isolated tables per call.""" + pytest.importorskip("mysql.connector") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1959,8 +2252,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_mysqlconnector_sync(mysql_service: MySQLService) -> Callable[..., Any]: +def adk_store_mysqlconnector_sync(mysql_service: MySQLService) -> "Callable[..., Any]": """Build a fresh mysql-connector sync ADK store with isolated tables per call.""" + pytest.importorskip("mysql.connector") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1975,8 +2270,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_asyncpg(postgres_service: PostgresService) -> Callable[..., Any]: +def adk_store_asyncpg(postgres_service: PostgresService) -> "Callable[..., Any]": """Build a fresh asyncpg ADK store with isolated tables per call.""" + pytest.importorskip("asyncpg") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -1990,8 +2287,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_psqlpy(postgres_service: PostgresService) -> Callable[..., Any]: +def adk_store_psqlpy(postgres_service: PostgresService) -> "Callable[..., Any]": """Build a fresh psqlpy ADK store with isolated tables per call.""" + pytest.importorskip("psqlpy") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2004,8 +2303,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_psycopg_async(postgres_service: PostgresService) -> Callable[..., Any]: +def adk_store_psycopg_async(postgres_service: PostgresService) -> "Callable[..., Any]": """Build a fresh psycopg async ADK store with isolated tables per call.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2019,8 +2321,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_psycopg_sync(postgres_service: PostgresService) -> Callable[..., Any]: +def adk_store_psycopg_sync(postgres_service: PostgresService) -> "Callable[..., Any]": """Build a fresh psycopg sync ADK store with isolated tables per call.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2034,8 +2339,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_oracle_async(oracle_23ai_service: OracleService) -> Callable[..., Any]: +def adk_store_oracle_async(oracle_23ai_service: OracleService) -> "Callable[..., Any]": """Build a fresh Oracle async ADK store with isolated tables per call.""" + pytest.importorskip("oracledb") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2048,8 +2355,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_oracle_sync(oracle_23ai_service: OracleService) -> Callable[..., Any]: +def adk_store_oracle_sync(oracle_23ai_service: OracleService) -> "Callable[..., Any]": """Build a fresh Oracle sync ADK store with isolated tables per call.""" + pytest.importorskip("oracledb") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2062,8 +2371,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_pymysql(mysql_service: MySQLService) -> Callable[..., Any]: +def adk_store_pymysql(mysql_service: MySQLService) -> "Callable[..., Any]": """Build a fresh PyMySQL ADK store with isolated tables per call.""" + pytest.importorskip("pymysql") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2076,8 +2387,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_cockroach_asyncpg(cockroachdb_service: CockroachDBService) -> Callable[..., Any]: +def adk_store_cockroach_asyncpg(cockroachdb_service: CockroachDBService) -> "Callable[..., Any]": """Build a fresh CockroachDB asyncpg ADK store with isolated tables per call.""" + pytest.importorskip("asyncpg") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2092,8 +2405,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_cockroach_psycopg_async(cockroachdb_service: CockroachDBService) -> Callable[..., Any]: +def adk_store_cockroach_psycopg_async(cockroachdb_service: CockroachDBService) -> "Callable[..., Any]": """Build a fresh CockroachDB psycopg async ADK store with isolated tables per call.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2107,8 +2423,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_cockroach_psycopg_sync(cockroachdb_service: CockroachDBService) -> Callable[..., Any]: +def adk_store_cockroach_psycopg_sync(cockroachdb_service: CockroachDBService) -> "Callable[..., Any]": """Build a fresh CockroachDB psycopg sync ADK store with isolated tables per call.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2122,8 +2441,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_adbc_sqlite(tmp_path: Path) -> Callable[..., Any]: +def adk_store_adbc_sqlite(tmp_path: Path) -> "Callable[..., Any]": """Build a fresh ADBC SQLite ADK store with isolated tables per call.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2138,8 +2460,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_adbc_duckdb(tmp_path: Path) -> Callable[..., Any]: +def adk_store_adbc_duckdb(tmp_path: Path) -> "Callable[..., Any]": """Build a fresh ADBC DuckDB ADK store with isolated tables per call.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2154,8 +2479,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_adbc_postgres(postgres_service: PostgresService) -> Callable[..., Any]: +def adk_store_adbc_postgres(postgres_service: PostgresService) -> "Callable[..., Any]": """Build a fresh ADBC PostgreSQL ADK store with isolated tables per call.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2170,8 +2498,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_arrow_odbc_mssql(mssql_service: MSSQLService) -> Callable[..., Any]: +def adk_store_arrow_odbc_mssql(mssql_service: MSSQLService) -> "Callable[..., Any]": """Build a fresh arrow-odbc SQL Server ADK store with isolated tables per call.""" + pytest.importorskip("arrow_odbc") + pytest.importorskip("pyarrow") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2186,8 +2517,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_mssql_python(mssql_service: MSSQLService) -> Callable[..., Any]: +def adk_store_mssql_python(mssql_service: MSSQLService) -> "Callable[..., Any]": """Build a fresh mssql-python ADK store with isolated tables per call.""" + pytest.importorskip("mssql_python") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2201,8 +2534,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_pymssql(mssql_service: MSSQLService) -> Callable[..., Any]: +def adk_store_pymssql(mssql_service: MSSQLService) -> "Callable[..., Any]": """Build a fresh pymssql ADK store with isolated tables per call.""" + pytest.importorskip("pymssql") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": suffix = uuid4().hex[:8] @@ -2221,6 +2556,7 @@ def _resolve_adk_store_case(request: pytest.FixtureRequest, case: AdkStoreCase) @pytest.fixture(params=ADK_STORE_PARAMS) def adk_store_case(request: pytest.FixtureRequest) -> AdkStoreCaseContext: """Resolve an ADK store contract case by factory fixture name.""" + pytest.importorskip("google.adk") return _resolve_adk_store_case(request, request.param) @@ -2249,7 +2585,9 @@ def _resolve_driver_case(request: pytest.FixtureRequest, case: DriverCase) -> Dr @pytest.fixture -def sync_postgres_extension_case(request: pytest.FixtureRequest) -> Generator[PostgresExtensionCaseContext, None, None]: +def sync_postgres_extension_case( + request: pytest.FixtureRequest, +) -> "Generator[PostgresExtensionCaseContext, None, None]": """Resolve a sync PostgreSQL extension contract case.""" case = request.param assert isinstance(case, PostgresExtensionCase) @@ -2261,7 +2599,7 @@ def sync_postgres_extension_case(request: pytest.FixtureRequest) -> Generator[Po @pytest.fixture async def async_postgres_extension_case( request: pytest.FixtureRequest, -) -> AsyncGenerator[PostgresExtensionCaseContext, None]: +) -> "AsyncGenerator[PostgresExtensionCaseContext, None]": """Resolve an async PostgreSQL extension contract case.""" case = request.param assert isinstance(case, PostgresExtensionCase) @@ -2328,21 +2666,22 @@ def driver_case(request: pytest.FixtureRequest) -> DriverCaseContext: ) -def _drop_db2_contract_table(driver: Db2SyncDriver) -> None: +def _drop_db2_contract_table(driver: "Db2SyncDriver") -> None: if driver.select_value(_DB2_CONTRACT_TABLE_EXISTS_SQL): driver.execute_script("DROP TABLE contract_items") driver.commit() -async def _drop_db2_contract_table_async(driver: Db2AsyncDriver) -> None: +async def _drop_db2_contract_table_async(driver: "Db2AsyncDriver") -> None: if await driver.select_value(_DB2_CONTRACT_TABLE_EXISTS_SQL): await driver.execute_script("DROP TABLE contract_items") await driver.commit() @pytest.fixture -def contract_db2_sync_driver(db2_sync_config: Db2SyncConfig) -> Generator[Db2SyncDriver, None, None]: +def contract_db2_sync_driver(db2_sync_config: "Db2SyncConfig") -> "Generator[Db2SyncDriver, None, None]": """Provide a fresh Db2 sync driver for contract tests.""" + pytest.importorskip("ibm_db") with db2_sync_config.provide_session() as driver: _drop_db2_contract_table(driver) driver.execute_script(DB2_CONTRACT_TABLE.create_sql) @@ -2353,8 +2692,9 @@ def contract_db2_sync_driver(db2_sync_config: Db2SyncConfig) -> Generator[Db2Syn @pytest.fixture -async def contract_db2_async_driver(db2_async_config: Db2AsyncConfig) -> "AsyncGenerator[Db2AsyncDriver, None]": +async def contract_db2_async_driver(db2_async_config: "Db2AsyncConfig") -> "AsyncGenerator[Db2AsyncDriver, None]": """Provide a fresh Db2 async driver for contract tests.""" + pytest.importorskip("ibm_db") async with db2_async_config.provide_session() as driver: await _drop_db2_contract_table_async(driver) await driver.execute_script(DB2_CONTRACT_TABLE.create_sql) @@ -2378,13 +2718,14 @@ def _db2_config_options( @pytest.fixture def lifecycle_config_db2_sync(db2_connection_config: "dict[str, Any]") -> "Callable[..., Db2SyncConfig]": """Build fresh Db2 sync configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("ibm_db") def make( *, pooled: bool = False, driver_features: "Db2DriverFeatures | None" = None, connection_instance: object | None = None, - ) -> Db2SyncConfig: + ) -> "Db2SyncConfig": return Db2SyncConfig( connection_config=dict(db2_connection_config), **_db2_config_options(driver_features, connection_instance) ) @@ -2395,13 +2736,14 @@ def make( @pytest.fixture def lifecycle_config_db2_async(db2_connection_config: "dict[str, Any]") -> "Callable[..., Db2AsyncConfig]": """Build fresh Db2 async configs for the pooling/connection-hook lifecycle contracts.""" + pytest.importorskip("ibm_db") def make( *, pooled: bool = False, driver_features: "Db2DriverFeatures | None" = None, connection_instance: object | None = None, - ) -> Db2AsyncConfig: + ) -> "Db2AsyncConfig": return Db2AsyncConfig( connection_config=dict(db2_connection_config), **_db2_config_options(driver_features, connection_instance) ) @@ -2412,6 +2754,8 @@ def make( @pytest.fixture async def contract_db2_sync_store(db2_connection_config: "dict[str, Any]") -> "AsyncGenerator[Db2SyncStore, None]": """Provide a ready Db2 sync Litestar store for contract tests.""" + pytest.importorskip("ibm_db") + pytest.importorskip("litestar") config = Db2SyncConfig(connection_config=dict(db2_connection_config), extension_config=_STORE_EXTENSION_CONFIG) store = Db2SyncStore(config) await store.create_table() @@ -2425,6 +2769,8 @@ async def contract_db2_sync_store(db2_connection_config: "dict[str, Any]") -> "A @pytest.fixture async def contract_db2_async_store(db2_connection_config: "dict[str, Any]") -> "AsyncGenerator[Db2AsyncStore, None]": """Provide a ready Db2 async Litestar store for contract tests.""" + pytest.importorskip("ibm_db") + pytest.importorskip("litestar") config = Db2AsyncConfig(connection_config=dict(db2_connection_config), extension_config=_STORE_EXTENSION_CONFIG) store = Db2AsyncStore(config) await store.create_table() @@ -2436,8 +2782,10 @@ async def contract_db2_async_store(db2_connection_config: "dict[str, Any]") -> " @pytest.fixture -def adk_store_db2_sync(db2_connection_config: "dict[str, Any]") -> Callable[..., Any]: +def adk_store_db2_sync(db2_connection_config: "dict[str, Any]") -> "Callable[..., Any]": """Build a fresh Db2 sync ADK store with isolated tables per call.""" + pytest.importorskip("ibm_db") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": config = Db2SyncConfig( @@ -2449,8 +2797,10 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def adk_store_db2_async(db2_connection_config: "dict[str, Any]") -> Callable[..., Any]: +def adk_store_db2_async(db2_connection_config: "dict[str, Any]") -> "Callable[..., Any]": """Build a fresh Db2 async ADK store with isolated tables per call.""" + pytest.importorskip("ibm_db") + pytest.importorskip("google.adk") def make() -> "tuple[Any, Any]": config = Db2AsyncConfig( @@ -2462,10 +2812,11 @@ def make() -> "tuple[Any, Any]": @pytest.fixture -def events_config_db2_sync(db2_connection_config: "dict[str, Any]", tmp_path: Path) -> Callable[..., Any]: +def events_config_db2_sync(db2_connection_config: "dict[str, Any]", tmp_path: Path) -> "Callable[..., Any]": """Build Db2 sync event-channel configs for contract tests.""" + pytest.importorskip("ibm_db") - def make(*, extension_config: "dict[str, Any]", suffix: str) -> Db2SyncConfig: + def make(*, extension_config: "dict[str, Any]", suffix: str) -> "Db2SyncConfig": return Db2SyncConfig( connection_config=dict(db2_connection_config), migration_config=_events_migration_config(tmp_path, suffix), @@ -2476,10 +2827,11 @@ def make(*, extension_config: "dict[str, Any]", suffix: str) -> Db2SyncConfig: @pytest.fixture -def events_config_db2_async(db2_connection_config: "dict[str, Any]", tmp_path: Path) -> Callable[..., Any]: +def events_config_db2_async(db2_connection_config: "dict[str, Any]", tmp_path: Path) -> "Callable[..., Any]": """Build Db2 async event-channel configs for contract tests.""" + pytest.importorskip("ibm_db") - def make(*, extension_config: "dict[str, Any]", suffix: str) -> Db2AsyncConfig: + def make(*, extension_config: "dict[str, Any]", suffix: str) -> "Db2AsyncConfig": return Db2AsyncConfig( connection_config=dict(db2_connection_config), migration_config=_events_migration_config(tmp_path, suffix), @@ -2490,10 +2842,11 @@ def make(*, extension_config: "dict[str, Any]", suffix: str) -> Db2AsyncConfig: @pytest.fixture -def migration_config_db2_sync(db2_connection_config: "dict[str, Any]") -> Callable[..., Any]: +def migration_config_db2_sync(db2_connection_config: "dict[str, Any]") -> "Callable[..., Any]": """Build Db2 sync configs for migration contract tests.""" + pytest.importorskip("ibm_db") - def make(*, script_location: str, version_table_name: str, suffix: str) -> Db2SyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "Db2SyncConfig": return Db2SyncConfig( connection_config=dict(db2_connection_config), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -2503,10 +2856,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Db2Sy @pytest.fixture -def migration_config_db2_async(db2_connection_config: "dict[str, Any]") -> Callable[..., Any]: +def migration_config_db2_async(db2_connection_config: "dict[str, Any]") -> "Callable[..., Any]": """Build Db2 async configs for migration contract tests.""" + pytest.importorskip("ibm_db") - def make(*, script_location: str, version_table_name: str, suffix: str) -> Db2AsyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "Db2AsyncConfig": return Db2AsyncConfig( connection_config=dict(db2_connection_config), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -2516,10 +2870,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Db2As @pytest.fixture -def migration_config_spanner_sync(spanner_service: SpannerService, spanner_database: Any) -> Callable[..., Any]: +def migration_config_spanner_sync(spanner_service: SpannerService, spanner_database: Any) -> "Callable[..., Any]": """Build Spanner sync configs for migration contract tests.""" + pytest.importorskip("google.cloud.spanner") - def make(*, script_location: str, version_table_name: str, suffix: str) -> SpannerSyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "SpannerSyncConfig": return SpannerSyncConfig( connection_config=build_spanner_connection_config(spanner_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, @@ -2529,10 +2884,11 @@ def make(*, script_location: str, version_table_name: str, suffix: str) -> Spann @pytest.fixture -def migration_config_spanner_async(spanner_service: SpannerService, spanner_database: Any) -> Callable[..., Any]: +def migration_config_spanner_async(spanner_service: SpannerService, spanner_database: Any) -> "Callable[..., Any]": """Build Spanner async configs for migration contract tests.""" + pytest.importorskip("google.cloud.spanner") - def make(*, script_location: str, version_table_name: str, suffix: str) -> SpannerAsyncConfig: + def make(*, script_location: str, version_table_name: str, suffix: str) -> "SpannerAsyncConfig": return SpannerAsyncConfig( connection_config=build_spanner_connection_config(spanner_service), migration_config={"script_location": script_location, "version_table_name": version_table_name}, diff --git a/tests/integration/fixtures/cockroach.py b/tests/integration/fixtures/cockroach.py new file mode 100644 index 000000000..f8192964a --- /dev/null +++ b/tests/integration/fixtures/cockroach.py @@ -0,0 +1,99 @@ +"""Shared CockroachDB integration fixtures.""" + +from typing import TYPE_CHECKING, Any + +import pytest + +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + + from pytest_databases.docker.cockroachdb import CockroachDBService + + from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgConfig + from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncConfig, CockroachPsycopgSyncConfig +else: + CockroachAsyncpgConfig = import_optional_attr("sqlspec.adapters.cockroach_asyncpg", "CockroachAsyncpgConfig") + CockroachPsycopgAsyncConfig = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgAsyncConfig" + ) + CockroachPsycopgSyncConfig = import_optional_attr( + "sqlspec.adapters.cockroach_psycopg", "CockroachPsycopgSyncConfig" + ) + +__all__ = ("cockroach_asyncpg_config", "cockroach_psycopg_async_config", "cockroach_psycopg_sync_config") + + +@pytest.fixture(scope="session") +async def cockroach_asyncpg_config( + cockroachdb_service: "CockroachDBService", +) -> "AsyncGenerator[CockroachAsyncpgConfig, None]": + """Provide a session-scoped CockroachDB asyncpg configuration.""" + pytest.importorskip("asyncpg") + connection_config = _cockroach_asyncpg_connection_config(cockroachdb_service) + connection_config.update({"min_size": 1, "max_size": 5}) + config = CockroachAsyncpgConfig(connection_config=connection_config) + try: + yield config + finally: + if config.connection_instance is not None: + await config.close_pool() + config.connection_instance = None + + +@pytest.fixture(scope="session") +async def cockroach_psycopg_async_config( + cockroachdb_service: "CockroachDBService", +) -> "AsyncGenerator[CockroachPsycopgAsyncConfig, None]": + """Provide a session-scoped CockroachDB psycopg async configuration.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + config = CockroachPsycopgAsyncConfig( + connection_config={"conninfo": _cockroach_conninfo(cockroachdb_service), "min_size": 1, "max_size": 5} + ) + try: + yield config + finally: + if config.connection_instance is not None: + await config.close_pool() + config.connection_instance = None + + +@pytest.fixture(scope="session") +def cockroach_psycopg_sync_config( + cockroachdb_service: "CockroachDBService", +) -> "Generator[CockroachPsycopgSyncConfig, None, None]": + """Provide a session-scoped CockroachDB psycopg sync configuration.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + config = CockroachPsycopgSyncConfig( + connection_config={"conninfo": _cockroach_conninfo(cockroachdb_service), "min_size": 1, "max_size": 5} + ) + try: + yield config + finally: + if config.connection_instance is not None: + config.close_pool() + + +def _cockroach_conninfo(cockroachdb_service: "CockroachDBService") -> str: + """Build a libpq conninfo string for a CockroachDB service container.""" + sslmode = cockroachdb_service.driver_opts.get("sslmode", "disable") + return ( + f"host={cockroachdb_service.host} port={cockroachdb_service.port} " + f"user=root dbname={cockroachdb_service.database} sslmode={sslmode}" + ) + + +def _cockroach_asyncpg_connection_config(cockroachdb_service: "CockroachDBService") -> dict[str, Any]: + """Build asyncpg connection parameters for a CockroachDB service container.""" + sslmode = cockroachdb_service.driver_opts.get("sslmode", "disable") + return { + "host": cockroachdb_service.host, + "port": cockroachdb_service.port, + "user": "root", + "password": "", + "database": cockroachdb_service.database, + "ssl": None if sslmode == "disable" else sslmode, + } diff --git a/tests/integration/fixtures/mssql.py b/tests/integration/fixtures/mssql.py index c23b9be67..d35ae6680 100644 --- a/tests/integration/fixtures/mssql.py +++ b/tests/integration/fixtures/mssql.py @@ -1,18 +1,28 @@ """Shared SQL Server integration fixtures.""" import contextlib -from collections.abc import AsyncGenerator, Callable, Generator from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest -from pytest_databases.docker.mssql import MSSQLService +from pytest_databases.docker.mssql import MSSQLService, exec_sqlcmd -from sqlspec.adapters.arrow_odbc import ArrowOdbcConfig -from sqlspec.adapters.mssql_python import MssqlPythonConfig -from sqlspec.adapters.mssql_python.litestar import MssqlPythonStore -from sqlspec.adapters.pymssql import PymssqlConfig -from sqlspec.adapters.pymssql.litestar import PymssqlStore +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable, Generator + + from sqlspec.adapters.arrow_odbc import ArrowOdbcConfig + from sqlspec.adapters.mssql_python import MssqlPythonConfig + from sqlspec.adapters.mssql_python.litestar import MssqlPythonStore + from sqlspec.adapters.pymssql import PymssqlConfig + from sqlspec.adapters.pymssql.litestar import PymssqlStore +else: + ArrowOdbcConfig = import_optional_attr("sqlspec.adapters.arrow_odbc", "ArrowOdbcConfig") + MssqlPythonConfig = import_optional_attr("sqlspec.adapters.mssql_python", "MssqlPythonConfig") + MssqlPythonStore = import_optional_attr("sqlspec.adapters.mssql_python.litestar", "MssqlPythonStore") + PymssqlConfig = import_optional_attr("sqlspec.adapters.pymssql", "PymssqlConfig") + PymssqlStore = import_optional_attr("sqlspec.adapters.pymssql.litestar", "PymssqlStore") __all__ = ( "MSSQL_MIGRATION_LOGIN", @@ -35,84 +45,45 @@ MSSQL_MIGRATION_PASSWORD = "Password123!" _STORE_TABLE = "litestar_contract_sessions" -_STORE_EXTENSION_CONFIG: "dict[str, Any]" = {"litestar": {"session_table": _STORE_TABLE}} - - -def _mssql_events_migration_config(tmp_path: Path, suffix: str) -> "dict[str, Any]": - migrations = tmp_path / f"migrations_{suffix}" - migrations.mkdir() - return { - "script_location": str(migrations), - "include_extensions": ["events"], - "version_table_name": f"ddl_migrations_{suffix}", - } +_STORE_EXTENSION_CONFIG: dict[str, Any] = {"litestar": {"session_table": _STORE_TABLE}} -def ensure_mssql_migration_login(mssql_service: "MSSQLService") -> None: +def ensure_mssql_migration_login(mssql_service: MSSQLService) -> None: """Ensure SQL Server migration login and database user exist with db_owner membership.""" - import pymssql - - conn = pymssql.connect( - server=mssql_service.host, - port=str(mssql_service.port), + sql = f""" + IF NOT EXISTS (SELECT * FROM sys.server_principals WHERE name = '{MSSQL_MIGRATION_LOGIN}') + BEGIN + CREATE LOGIN [{MSSQL_MIGRATION_LOGIN}] WITH PASSWORD = '{MSSQL_MIGRATION_PASSWORD}', CHECK_POLICY = OFF; + END + IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '{MSSQL_MIGRATION_LOGIN}') + BEGIN + CREATE USER [{MSSQL_MIGRATION_LOGIN}] FOR LOGIN [{MSSQL_MIGRATION_LOGIN}]; + ALTER ROLE [db_owner] ADD MEMBER [{MSSQL_MIGRATION_LOGIN}]; + END + """ + exit_code, output = exec_sqlcmd( + mssql_service.container, + sql, + database=mssql_service.database, user=mssql_service.user, password=mssql_service.password, - database=mssql_service.database, - autocommit=True, ) - try: - with conn.cursor() as cursor: - cursor.execute( - f""" - IF NOT EXISTS (SELECT * FROM sys.server_principals WHERE name = '{MSSQL_MIGRATION_LOGIN}') - BEGIN - CREATE LOGIN [{MSSQL_MIGRATION_LOGIN}] WITH PASSWORD = '{MSSQL_MIGRATION_PASSWORD}', CHECK_POLICY = OFF; - END - IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '{MSSQL_MIGRATION_LOGIN}') - BEGIN - CREATE USER [{MSSQL_MIGRATION_LOGIN}] FOR LOGIN [{MSSQL_MIGRATION_LOGIN}]; - ALTER ROLE [db_owner] ADD MEMBER [{MSSQL_MIGRATION_LOGIN}]; - END - """ - ) - finally: - conn.close() - - -def _mssql_connection_config(mssql_service: "MSSQLService") -> "dict[str, Any]": - return { - "server": mssql_service.host, - "port": mssql_service.port, - "database": mssql_service.database, - "user": mssql_service.user, - "password": mssql_service.password, - } - - -def _mssql_python_connection_config(mssql_service: "MSSQLService", *, autocommit: bool = True) -> "dict[str, Any]": - connection_config = _mssql_connection_config(mssql_service) - connection_config.update({ - "encrypt": False, - "trust_server_certificate": True, - "autocommit": autocommit, - "pool_enabled": False, - }) - return connection_config - - -def _arrow_odbc_connection_config(mssql_service: "MSSQLService") -> "dict[str, str]": - return {"connection_string": mssql_service.connection_string} + if exit_code != 0: + decoded_output = output.decode("utf-8", errors="replace") + msg = f"Failed to provision SQL Server migration login {MSSQL_MIGRATION_LOGIN!r}: {decoded_output}" + raise RuntimeError(msg) @pytest.fixture(scope="session") -def mssql_python_connection_config(mssql_service: "MSSQLService") -> "dict[str, Any]": +def mssql_python_connection_config(mssql_service: MSSQLService) -> dict[str, Any]: """Provide shared mssql-python connection parameters.""" return _mssql_python_connection_config(mssql_service, autocommit=False) @pytest.fixture(scope="session") -def mssql_python_config(mssql_python_connection_config: "dict[str, Any]") -> "Generator[MssqlPythonConfig, None, None]": +def mssql_python_config(mssql_python_connection_config: dict[str, Any]) -> "Generator[MssqlPythonConfig, None, None]": """Provide a session-scoped mssql-python configuration.""" + pytest.importorskip("mssql_python") config = MssqlPythonConfig(connection_config=dict(mssql_python_connection_config)) try: yield config @@ -121,14 +92,15 @@ def mssql_python_config(mssql_python_connection_config: "dict[str, Any]") -> "Ge @pytest.fixture(scope="session") -def pymssql_connection_config(mssql_service: "MSSQLService") -> "dict[str, Any]": +def pymssql_connection_config(mssql_service: MSSQLService) -> dict[str, Any]: """Provide shared pymssql connection parameters.""" return _mssql_connection_config(mssql_service) @pytest.fixture(scope="session") -def pymssql_config(pymssql_connection_config: "dict[str, Any]") -> "Generator[PymssqlConfig, None, None]": +def pymssql_config(pymssql_connection_config: dict[str, Any]) -> "Generator[PymssqlConfig, None, None]": """Provide a session-scoped pymssql configuration.""" + pytest.importorskip("pymssql") config = PymssqlConfig(connection_config=dict(pymssql_connection_config)) try: yield config @@ -137,8 +109,10 @@ def pymssql_config(pymssql_connection_config: "dict[str, Any]") -> "Generator[Py @pytest.fixture(scope="session") -def arrow_odbc_mssql_config(mssql_service: "MSSQLService") -> "Generator[ArrowOdbcConfig, None, None]": +def arrow_odbc_mssql_config(mssql_service: MSSQLService) -> "Generator[ArrowOdbcConfig, None, None]": """Provide a session-scoped arrow-odbc SQL Server configuration.""" + pytest.importorskip("arrow_odbc") + pytest.importorskip("pyarrow") config = ArrowOdbcConfig( connection_config=_arrow_odbc_connection_config(mssql_service), driver_features={"dbms_name": "Microsoft SQL Server"}, @@ -150,10 +124,11 @@ def arrow_odbc_mssql_config(mssql_service: "MSSQLService") -> "Generator[ArrowOd @pytest.fixture -def events_config_mssql_python(mssql_service: "MSSQLService", tmp_path: Path) -> "Callable[..., Any]": +def events_config_mssql_python(mssql_service: MSSQLService, tmp_path: Path) -> "Callable[..., Any]": """Build mssql-python event-channel configs for contract tests.""" + pytest.importorskip("mssql_python") - def make(*, extension_config: "dict[str, Any]", suffix: str) -> MssqlPythonConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "MssqlPythonConfig": return MssqlPythonConfig( connection_config=_mssql_python_connection_config(mssql_service), migration_config=_mssql_events_migration_config(tmp_path, suffix), @@ -164,10 +139,11 @@ def make(*, extension_config: "dict[str, Any]", suffix: str) -> MssqlPythonConfi @pytest.fixture -def events_config_pymssql(mssql_service: "MSSQLService", tmp_path: Path) -> "Callable[..., Any]": +def events_config_pymssql(mssql_service: MSSQLService, tmp_path: Path) -> "Callable[..., Any]": """Build pymssql event-channel configs for contract tests.""" + pytest.importorskip("pymssql") - def make(*, extension_config: "dict[str, Any]", suffix: str) -> PymssqlConfig: + def make(*, extension_config: dict[str, Any], suffix: str) -> "PymssqlConfig": return PymssqlConfig( connection_config=_mssql_connection_config(mssql_service), migration_config=_mssql_events_migration_config(tmp_path, suffix), @@ -178,8 +154,10 @@ def make(*, extension_config: "dict[str, Any]", suffix: str) -> PymssqlConfig: @pytest.fixture -async def contract_mssql_python_store(mssql_service: "MSSQLService") -> "AsyncGenerator[MssqlPythonStore, None]": +async def contract_mssql_python_store(mssql_service: MSSQLService) -> "AsyncGenerator[MssqlPythonStore, None]": """Provide a ready mssql-python Litestar store for contract tests.""" + pytest.importorskip("mssql_python") + pytest.importorskip("litestar") config = MssqlPythonConfig( connection_config=_mssql_python_connection_config(mssql_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -202,8 +180,10 @@ async def contract_mssql_python_store(mssql_service: "MSSQLService") -> "AsyncGe @pytest.fixture -async def contract_pymssql_store(mssql_service: "MSSQLService") -> "AsyncGenerator[PymssqlStore, None]": +async def contract_pymssql_store(mssql_service: MSSQLService) -> "AsyncGenerator[PymssqlStore, None]": """Provide a ready pymssql Litestar store for contract tests.""" + pytest.importorskip("pymssql") + pytest.importorskip("litestar") config = PymssqlConfig( connection_config=_mssql_connection_config(mssql_service), extension_config=_STORE_EXTENSION_CONFIG ) @@ -226,7 +206,7 @@ async def contract_pymssql_store(mssql_service: "MSSQLService") -> "AsyncGenerat @pytest.fixture(scope="session") -def mssql_migration_connection_config(mssql_service: "MSSQLService") -> "dict[str, Any]": +def mssql_migration_connection_config(mssql_service: MSSQLService) -> dict[str, Any]: """Provide SQL Server connection parameters using the dedicated migration login.""" ensure_mssql_migration_login(mssql_service) config = _mssql_connection_config(mssql_service) @@ -236,10 +216,45 @@ def mssql_migration_connection_config(mssql_service: "MSSQLService") -> "dict[st @pytest.fixture(scope="session") -def mssql_python_migration_connection_config(mssql_service: "MSSQLService") -> "dict[str, Any]": +def mssql_python_migration_connection_config(mssql_service: MSSQLService) -> dict[str, Any]: """Provide mssql-python connection parameters using the dedicated migration login.""" ensure_mssql_migration_login(mssql_service) config = _mssql_python_connection_config(mssql_service, autocommit=False) config["user"] = MSSQL_MIGRATION_LOGIN config["password"] = MSSQL_MIGRATION_PASSWORD return config + + +def _mssql_events_migration_config(tmp_path: Path, suffix: str) -> dict[str, Any]: + migrations = tmp_path / f"migrations_{suffix}" + migrations.mkdir() + return { + "script_location": str(migrations), + "include_extensions": ["events"], + "version_table_name": f"ddl_migrations_{suffix}", + } + + +def _mssql_connection_config(mssql_service: MSSQLService) -> dict[str, Any]: + return { + "server": mssql_service.host, + "port": mssql_service.port, + "database": mssql_service.database, + "user": mssql_service.user, + "password": mssql_service.password, + } + + +def _mssql_python_connection_config(mssql_service: MSSQLService, *, autocommit: bool = True) -> dict[str, Any]: + connection_config = _mssql_connection_config(mssql_service) + connection_config.update({ + "encrypt": False, + "trust_server_certificate": True, + "autocommit": autocommit, + "pool_enabled": False, + }) + return connection_config + + +def _arrow_odbc_connection_config(mssql_service: MSSQLService) -> dict[str, str]: + return {"connection_string": mssql_service.connection_string} diff --git a/tests/integration/fixtures/oracle.py b/tests/integration/fixtures/oracle.py index 9a6623f34..83ef92ffb 100644 --- a/tests/integration/fixtures/oracle.py +++ b/tests/integration/fixtures/oracle.py @@ -1,17 +1,30 @@ """Shared Oracle integration fixtures.""" -from collections.abc import AsyncGenerator, Generator +import shlex +from typing import TYPE_CHECKING, cast import pytest from pytest_databases.docker.oracle import OracleService +from pytest_databases.helpers import exec_output_to_bytes -from sqlspec.adapters.oracledb import ( - OracleAsyncConfig, - OracleAsyncDriver, - OraclePoolParams, - OracleSyncConfig, - OracleSyncDriver, -) +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + + from sqlspec.adapters.oracledb import ( + OracleAsyncConfig, + OracleAsyncDriver, + OraclePoolParams, + OracleSyncConfig, + OracleSyncDriver, + ) +else: + OracleAsyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncConfig") + OracleAsyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncDriver") + OraclePoolParams = import_optional_attr("sqlspec.adapters.oracledb", "OraclePoolParams") + OracleSyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncConfig") + OracleSyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncDriver") __all__ = ( "oracle_18c_async_config", @@ -26,22 +39,8 @@ ) -def _oracle_pool_params(oracle_service: "OracleService") -> "OraclePoolParams": - return OraclePoolParams( - host=oracle_service.host, - port=oracle_service.port, - service_name=oracle_service.service_name, - user=oracle_service.user, - password=oracle_service.password, - min=1, - max=5, - retry_count=5, - retry_delay=1, - ) - - @pytest.fixture(scope="session") -def oracle_18c_connection_config(oracle_18c_service: "OracleService") -> "OraclePoolParams": +def oracle_18c_connection_config(oracle_18c_service: OracleService) -> "OraclePoolParams": """Provide Oracle 18c pool parameters for non-native JSON storage coverage.""" return _oracle_pool_params(oracle_18c_service) @@ -51,6 +50,7 @@ async def oracle_18c_async_config( oracle_18c_connection_config: "OraclePoolParams", ) -> "AsyncGenerator[OracleAsyncConfig, None]": """Provide a session-scoped Oracle 18c async configuration.""" + pytest.importorskip("oracledb") config = OracleAsyncConfig(connection_config=OraclePoolParams(**oracle_18c_connection_config)) try: yield config @@ -65,45 +65,44 @@ async def oracle_18c_async_session( oracle_18c_async_config: "OracleAsyncConfig", ) -> "AsyncGenerator[OracleAsyncDriver, None]": """Create an Oracle 18c async driver session.""" + pytest.importorskip("oracledb") async with oracle_18c_async_config.provide_session() as driver: yield driver @pytest.fixture(scope="session") -def oracle_connection_config(oracle_23ai_service: "OracleService") -> "OraclePoolParams": +def oracle_connection_config(oracle_23ai_service: OracleService) -> "OraclePoolParams": """Provide shared Oracle pool parameters.""" return _oracle_pool_params(oracle_23ai_service) @pytest.fixture(scope="session") -def oracle_aq_privileges(oracle_23ai_service: "OracleService") -> None: - """Grant the application user privileges required by Oracle AQ tests.""" +def oracle_aq_privileges(oracle_23ai_service: OracleService) -> None: + """Grant the application user privileges required by Oracle AQ tests via in-container sqlplus.""" app_user = oracle_23ai_service.user - config = OracleSyncConfig( - connection_config={ - "host": oracle_23ai_service.host, - "port": oracle_23ai_service.port, - "service_name": oracle_23ai_service.service_name, - "user": "system", - "password": oracle_23ai_service.system_password, - "retry_count": 5, - "retry_delay": 1, - } - ) - grants = (f"GRANT aq_administrator_role, aq_user_role TO {app_user}", f"GRANT EXECUTE ON dbms_aq TO {app_user}") - try: - with config.provide_session() as driver: - for grant in grants: - driver.execute_script(grant) - driver.commit() - finally: - if config.connection_instance is not None: - config.close_pool() + script = "\n".join(( + "SET HEADING OFF FEEDBACK OFF PAGESIZE 0 VERIFY OFF ECHO OFF", + "WHENEVER OSERROR EXIT 9", + "WHENEVER SQLERROR EXIT SQL.SQLCODE", + f"GRANT aq_administrator_role, aq_user_role TO {app_user};", + f"GRANT EXECUTE ON dbms_aq TO {app_user};", + "EXIT", + "", + )) + connect_string = f"system/{oracle_23ai_service.system_password}@//localhost:1521/{oracle_23ai_service.service_name}" + command = f"printf '%s' {shlex.quote(script)} | sqlplus -L -S {shlex.quote(connect_string)}" + result = oracle_23ai_service.container.exec_run(["bash", "-lc", command]) + exit_code = result.exit_code if result.exit_code is not None else -1 + if exit_code != 0: + decoded_output = exec_output_to_bytes(result.output).decode("utf-8", errors="replace") + msg = f"Failed to grant Oracle AQ privileges to {app_user!r}: {decoded_output}" + raise RuntimeError(msg) @pytest.fixture(scope="session") def oracle_sync_config(oracle_connection_config: "OraclePoolParams") -> "Generator[OracleSyncConfig, None, None]": """Provide a session-scoped Oracle sync configuration.""" + pytest.importorskip("oracledb") config = OracleSyncConfig(connection_config=OraclePoolParams(**oracle_connection_config)) try: yield config @@ -117,6 +116,7 @@ async def oracle_async_config( oracle_connection_config: "OraclePoolParams", ) -> "AsyncGenerator[OracleAsyncConfig, None]": """Provide a session-scoped Oracle async configuration.""" + pytest.importorskip("oracledb") config = OracleAsyncConfig(connection_config=OraclePoolParams(**oracle_connection_config)) try: yield config @@ -129,6 +129,7 @@ async def oracle_async_config( @pytest.fixture def oracle_sync_session(oracle_sync_config: "OracleSyncConfig") -> "Generator[OracleSyncDriver, None, None]": """Create an Oracle sync driver session.""" + pytest.importorskip("oracledb") with oracle_sync_config.provide_session() as driver: yield driver @@ -136,5 +137,23 @@ def oracle_sync_session(oracle_sync_config: "OracleSyncConfig") -> "Generator[Or @pytest.fixture async def oracle_async_session(oracle_async_config: "OracleAsyncConfig") -> "AsyncGenerator[OracleAsyncDriver, None]": """Create an Oracle async driver session.""" + pytest.importorskip("oracledb") async with oracle_async_config.provide_session() as driver: yield driver + + +def _oracle_pool_params(oracle_service: OracleService) -> "OraclePoolParams": + return cast( + "OraclePoolParams", + { + "host": oracle_service.host, + "port": oracle_service.port, + "service_name": oracle_service.service_name, + "user": oracle_service.user, + "password": oracle_service.password, + "min": 1, + "max": 5, + "retry_count": 5, + "retry_delay": 1, + }, + ) diff --git a/tests/integration/fixtures/postgres.py b/tests/integration/fixtures/postgres.py index 02ff48fe0..dec53c1b5 100644 --- a/tests/integration/fixtures/postgres.py +++ b/tests/integration/fixtures/postgres.py @@ -1,18 +1,30 @@ """Shared PostgreSQL-family integration fixtures.""" -from collections.abc import AsyncGenerator, Generator -from typing import Any, cast +from typing import TYPE_CHECKING, Any import pytest -from pytest_databases.docker.cockroachdb import CockroachDBService -from pytest_databases.docker.postgres import PostgresService - -from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver -from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver -from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgConfig -from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncConfig, CockroachPsycopgSyncConfig -from sqlspec.adapters.psqlpy import PsqlpyConfig, PsqlpyDriver, PsqlpyPoolParams -from sqlspec.adapters.psycopg import PsycopgAsyncConfig, PsycopgPoolParams, PsycopgSyncConfig +from pytest_databases.docker.postgres import PostgresService, exec_psql + +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + + from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver + from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver + from sqlspec.adapters.psqlpy import PsqlpyConfig, PsqlpyDriver, PsqlpyPoolParams + from sqlspec.adapters.psycopg import PsycopgAsyncConfig, PsycopgPoolParams, PsycopgSyncConfig +else: + AdbcConfig = import_optional_attr("sqlspec.adapters.adbc", "AdbcConfig") + AdbcDriver = import_optional_attr("sqlspec.adapters.adbc", "AdbcDriver") + AsyncpgConfig = import_optional_attr("sqlspec.adapters.asyncpg", "AsyncpgConfig") + AsyncpgDriver = import_optional_attr("sqlspec.adapters.asyncpg", "AsyncpgDriver") + PsqlpyConfig = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyConfig") + PsqlpyDriver = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyDriver") + PsqlpyPoolParams = import_optional_attr("sqlspec.adapters.psqlpy", "PsqlpyPoolParams") + PsycopgAsyncConfig = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgAsyncConfig") + PsycopgPoolParams = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgPoolParams") + PsycopgSyncConfig = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgSyncConfig") __all__ = ( "adbc_postgres_config", @@ -21,9 +33,6 @@ "asyncpg_async_driver", "asyncpg_config", "asyncpg_connection_config", - "cockroach_asyncpg_config", - "cockroach_psycopg_async_config", - "cockroach_psycopg_sync_config", "paradedb_config_adbc", "paradedb_config_asyncpg", "paradedb_config_psqlpy", @@ -40,73 +49,16 @@ ) -def _postgres_connection_config(postgres_service: "PostgresService") -> "dict[str, Any]": - return { - "host": postgres_service.host, - "port": postgres_service.port, - "user": postgres_service.user, - "password": postgres_service.password, - "database": postgres_service.database, - } - - -def _postgres_conninfo(postgres_service: "PostgresService") -> str: - return ( - f"postgresql://{postgres_service.user}:{postgres_service.password}" - f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" - ) - - -def _psqlpy_dsn(postgres_service: "PostgresService") -> str: - return ( - f"postgres://{postgres_service.user}:{postgres_service.password}" - f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" - ) - - -def _adbc_postgres_uri(postgres_service: "PostgresService") -> str: - return _postgres_conninfo(postgres_service) - - -def _asyncpg_pool_config(postgres_service: "PostgresService") -> "dict[str, Any]": - return _postgres_connection_config(postgres_service) - - -def _cockroach_conninfo(cockroachdb_service: "CockroachDBService") -> str: - return ( - f"host={cockroachdb_service.host} port={cockroachdb_service.port} " - f"user=root dbname={cockroachdb_service.database} sslmode=disable" - ) - - -def _cockroach_asyncpg_connection_config(cockroachdb_service: "CockroachDBService") -> "dict[str, Any]": - return { - "host": cockroachdb_service.host, - "port": cockroachdb_service.port, - "user": "root", - "password": "", - "database": cockroachdb_service.database, - "ssl": None, - } - - -def _ensure_postgres_extension(postgres_service: "PostgresService", extension: str) -> None: - import psycopg - - with psycopg.connect(_postgres_conninfo(postgres_service)) as conn: - cast("Any", conn).execute(f"CREATE EXTENSION IF NOT EXISTS {extension}") - conn.commit() - - @pytest.fixture(scope="session") -def asyncpg_connection_config(postgres_service: "PostgresService") -> "dict[str, Any]": +def asyncpg_connection_config(postgres_service: PostgresService) -> dict[str, Any]: """Base pool configuration for AsyncPG tests.""" return _postgres_connection_config(postgres_service) @pytest.fixture(scope="session") -async def asyncpg_config(asyncpg_connection_config: "dict[str, Any]") -> "AsyncGenerator[AsyncpgConfig, None]": +async def asyncpg_config(asyncpg_connection_config: dict[str, Any]) -> "AsyncGenerator[AsyncpgConfig, None]": """Provide a session-scoped AsyncpgConfig with a shared pool.""" + pytest.importorskip("asyncpg") config = AsyncpgConfig(connection_config=dict(asyncpg_connection_config)) try: yield config @@ -119,13 +71,16 @@ async def asyncpg_config(asyncpg_connection_config: "dict[str, Any]") -> "AsyncG @pytest.fixture async def asyncpg_async_driver(asyncpg_config: "AsyncpgConfig") -> "AsyncGenerator[AsyncpgDriver, None]": """Create an AsyncPG driver for integration tests.""" + pytest.importorskip("asyncpg") async with asyncpg_config.provide_session() as session: yield session @pytest.fixture(scope="session") -def psycopg_sync_config(postgres_service: "PostgresService") -> "Generator[PsycopgSyncConfig, None, None]": +def psycopg_sync_config(postgres_service: PostgresService) -> "Generator[PsycopgSyncConfig, None, None]": """Create a psycopg sync configuration.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") config = PsycopgSyncConfig(connection_config={"conninfo": _postgres_conninfo(postgres_service)}) try: yield config @@ -135,8 +90,10 @@ def psycopg_sync_config(postgres_service: "PostgresService") -> "Generator[Psyco @pytest.fixture(scope="session") -async def psycopg_async_config(postgres_service: "PostgresService") -> "AsyncGenerator[PsycopgAsyncConfig, None]": +async def psycopg_async_config(postgres_service: PostgresService) -> "AsyncGenerator[PsycopgAsyncConfig, None]": """Create a psycopg async configuration.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") config = PsycopgAsyncConfig( connection_config={"conninfo": _postgres_conninfo(postgres_service), "min_size": 1, "max_size": 4} ) @@ -152,8 +109,9 @@ async def psycopg_async_config(postgres_service: "PostgresService") -> "AsyncGen @pytest.fixture(scope="session") -async def psqlpy_config(postgres_service: "PostgresService") -> "AsyncGenerator[PsqlpyConfig, None]": +async def psqlpy_config(postgres_service: PostgresService) -> "AsyncGenerator[PsqlpyConfig, None]": """Provide a session-scoped psqlpy configuration.""" + pytest.importorskip("psqlpy") config = PsqlpyConfig(connection_config={"dsn": _psqlpy_dsn(postgres_service), "max_db_pool_size": 5}) try: yield config @@ -163,56 +121,10 @@ async def psqlpy_config(postgres_service: "PostgresService") -> "AsyncGenerator[ config.connection_instance = None -@pytest.fixture(scope="session") -async def cockroach_asyncpg_config( - cockroachdb_service: "CockroachDBService", -) -> "AsyncGenerator[CockroachAsyncpgConfig, None]": - """Provide a session-scoped CockroachDB asyncpg configuration.""" - connection_config = _cockroach_asyncpg_connection_config(cockroachdb_service) - connection_config.update({"min_size": 1, "max_size": 5}) - config = CockroachAsyncpgConfig(connection_config=connection_config) - try: - yield config - finally: - if config.connection_instance is not None: - await config.close_pool() - config.connection_instance = None - - -@pytest.fixture(scope="session") -async def cockroach_psycopg_async_config( - cockroachdb_service: "CockroachDBService", -) -> "AsyncGenerator[CockroachPsycopgAsyncConfig, None]": - """Provide a session-scoped CockroachDB psycopg async configuration.""" - config = CockroachPsycopgAsyncConfig( - connection_config={"conninfo": _cockroach_conninfo(cockroachdb_service), "min_size": 1, "max_size": 5} - ) - try: - yield config - finally: - if config.connection_instance is not None: - await config.close_pool() - config.connection_instance = None - - -@pytest.fixture(scope="session") -def cockroach_psycopg_sync_config( - cockroachdb_service: "CockroachDBService", -) -> "Generator[CockroachPsycopgSyncConfig, None, None]": - """Provide a session-scoped CockroachDB psycopg sync configuration.""" - config = CockroachPsycopgSyncConfig( - connection_config={"conninfo": _cockroach_conninfo(cockroachdb_service), "min_size": 1, "max_size": 5} - ) - try: - yield config - finally: - if config.connection_instance is not None: - config.close_pool() - - @pytest.fixture async def psqlpy_driver(psqlpy_config: "PsqlpyConfig") -> "AsyncGenerator[PsqlpyDriver, None]": """Yield a raw psqlpy driver session.""" + pytest.importorskip("psqlpy") async with psqlpy_config.provide_session() as session: yield session @@ -220,6 +132,7 @@ async def psqlpy_driver(psqlpy_config: "PsqlpyConfig") -> "AsyncGenerator[Psqlpy @pytest.fixture async def psqlpy_session(psqlpy_config: "PsqlpyConfig") -> "AsyncGenerator[PsqlpyDriver, None]": """Create a psqlpy session with test table setup and cleanup.""" + pytest.importorskip("psqlpy") async with psqlpy_config.provide_session() as session: await session.execute_script( """ @@ -239,14 +152,16 @@ async def psqlpy_session(psqlpy_config: "PsqlpyConfig") -> "AsyncGenerator[Psqlp @pytest.fixture(scope="session") -def adbc_postgres_connection_config(postgres_service: "PostgresService") -> "dict[str, str]": +def adbc_postgres_connection_config(postgres_service: PostgresService) -> dict[str, str]: """Shared PostgreSQL connection configuration for ADBC tests.""" return {"uri": _adbc_postgres_uri(postgres_service)} @pytest.fixture(scope="session") -def adbc_postgres_config(adbc_postgres_connection_config: "dict[str, str]") -> "Generator[AdbcConfig, None, None]": +def adbc_postgres_config(adbc_postgres_connection_config: dict[str, str]) -> "Generator[AdbcConfig, None, None]": """Provide an ADBC config targeting PostgreSQL.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") config = AdbcConfig(connection_config=dict(adbc_postgres_connection_config)) try: yield config @@ -257,28 +172,19 @@ def adbc_postgres_config(adbc_postgres_connection_config: "dict[str, str]") -> " @pytest.fixture def adbc_sync_driver(adbc_postgres_config: "AdbcConfig") -> "Generator[AdbcDriver, None, None]": """Create an ADBC driver for data dictionary testing.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") with adbc_postgres_config.provide_session() as session: yield session @pytest.fixture -def pgvector_config_adbc(pgvector_service: "PostgresService") -> "Generator[AdbcConfig, None, None]": +def pgvector_config_adbc(pgvector_service: PostgresService) -> "Generator[AdbcConfig, None, None]": """Provide an ADBC config connected to a pgvector-enabled PostgreSQL service.""" - from sqlspec.adapters.adbc.core import build_connection_config, resolve_driver_connect_func - - connection_config = {"uri": _adbc_postgres_uri(pgvector_service)} - conn = resolve_driver_connect_func(None, connection_config["uri"])(**build_connection_config(connection_config)) - try: - cursor = conn.cursor() - try: - cursor.execute("CREATE EXTENSION IF NOT EXISTS vector") - finally: - cursor.close() - conn.commit() - finally: - conn.close() - - config = AdbcConfig(connection_config=connection_config) + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") + _ensure_postgres_extension(pgvector_service, "vector") + config = AdbcConfig(connection_config={"uri": _adbc_postgres_uri(pgvector_service)}) try: yield config finally: @@ -286,30 +192,30 @@ def pgvector_config_adbc(pgvector_service: "PostgresService") -> "Generator[Adbc @pytest.fixture -def pgvector_config_asyncpg(pgvector_service: "PostgresService") -> "AsyncpgConfig": +def pgvector_config_asyncpg(pgvector_service: PostgresService) -> "AsyncpgConfig": """Provide an asyncpg config connected to a pgvector-enabled PostgreSQL service.""" + pytest.importorskip("asyncpg") _ensure_postgres_extension(pgvector_service, "vector") return AsyncpgConfig(connection_config=_asyncpg_pool_config(pgvector_service)) @pytest.fixture -def pgvector_config_psqlpy(pgvector_service: "PostgresService") -> "PsqlpyConfig": +def pgvector_config_psqlpy(pgvector_service: PostgresService) -> "PsqlpyConfig": """Provide a psqlpy config connected to a pgvector-enabled PostgreSQL service.""" + pytest.importorskip("psqlpy") _ensure_postgres_extension(pgvector_service, "vector") return PsqlpyConfig(connection_config=PsqlpyPoolParams(dsn=_psqlpy_dsn(pgvector_service))) @pytest.fixture -def pgvector_config_psycopg(pgvector_service: "PostgresService") -> "Generator[PsycopgSyncConfig, None, None]": +def pgvector_config_psycopg(pgvector_service: PostgresService) -> "Generator[PsycopgSyncConfig, None, None]": """Provide a psycopg config connected to a pgvector-enabled PostgreSQL service.""" - import psycopg - - connection_config = PsycopgPoolParams(conninfo=_postgres_conninfo(pgvector_service), min_size=1, max_size=4) - with psycopg.connect(connection_config["conninfo"]) as conn: - conn.execute("CREATE EXTENSION IF NOT EXISTS vector") - conn.commit() - - config = PsycopgSyncConfig(connection_config=connection_config) + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + _ensure_postgres_extension(pgvector_service, "vector") + config = PsycopgSyncConfig( + connection_config=PsycopgPoolParams(conninfo=_postgres_conninfo(pgvector_service), min_size=1, max_size=4) + ) try: yield config finally: @@ -319,8 +225,10 @@ def pgvector_config_psycopg(pgvector_service: "PostgresService") -> "Generator[P @pytest.fixture -def paradedb_config_adbc(paradedb_service: "PostgresService") -> "Generator[AdbcConfig, None, None]": +def paradedb_config_adbc(paradedb_service: PostgresService) -> "Generator[AdbcConfig, None, None]": """Provide an ADBC config connected to a ParadeDB service.""" + pytest.importorskip("adbc_driver_manager") + pytest.importorskip("pyarrow") config = AdbcConfig(connection_config={"uri": _adbc_postgres_uri(paradedb_service)}) try: yield config @@ -329,20 +237,24 @@ def paradedb_config_adbc(paradedb_service: "PostgresService") -> "Generator[Adbc @pytest.fixture -def paradedb_config_asyncpg(paradedb_service: "PostgresService") -> "AsyncpgConfig": +def paradedb_config_asyncpg(paradedb_service: PostgresService) -> "AsyncpgConfig": """Provide an asyncpg config connected to a ParadeDB service.""" + pytest.importorskip("asyncpg") return AsyncpgConfig(connection_config=_asyncpg_pool_config(paradedb_service)) @pytest.fixture -def paradedb_config_psqlpy(paradedb_service: "PostgresService") -> "PsqlpyConfig": +def paradedb_config_psqlpy(paradedb_service: PostgresService) -> "PsqlpyConfig": """Provide a psqlpy config connected to a ParadeDB service.""" + pytest.importorskip("psqlpy") return PsqlpyConfig(connection_config=PsqlpyPoolParams(dsn=_psqlpy_dsn(paradedb_service))) @pytest.fixture -def paradedb_config_psycopg(paradedb_service: "PostgresService") -> "Generator[PsycopgSyncConfig, None, None]": +def paradedb_config_psycopg(paradedb_service: PostgresService) -> "Generator[PsycopgSyncConfig, None, None]": """Provide a psycopg config connected to a ParadeDB service.""" + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") config = PsycopgSyncConfig( connection_config=PsycopgPoolParams(conninfo=_postgres_conninfo(paradedb_service), min_size=1, max_size=4) ) @@ -352,3 +264,50 @@ def paradedb_config_psycopg(paradedb_service: "PostgresService") -> "Generator[P if config.connection_instance is not None: config.close_pool() config.connection_instance = None + + +def _postgres_connection_config(postgres_service: PostgresService) -> dict[str, Any]: + return { + "host": postgres_service.host, + "port": postgres_service.port, + "user": postgres_service.user, + "password": postgres_service.password, + "database": postgres_service.database, + } + + +def _postgres_conninfo(postgres_service: PostgresService) -> str: + return ( + f"postgresql://{postgres_service.user}:{postgres_service.password}" + f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + ) + + +def _psqlpy_dsn(postgres_service: PostgresService) -> str: + return ( + f"postgres://{postgres_service.user}:{postgres_service.password}" + f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + ) + + +def _adbc_postgres_uri(postgres_service: PostgresService) -> str: + return _postgres_conninfo(postgres_service) + + +def _asyncpg_pool_config(postgres_service: PostgresService) -> dict[str, Any]: + return _postgres_connection_config(postgres_service) + + +def _ensure_postgres_extension(service: PostgresService, extension: str) -> None: + """Ensure a PostgreSQL extension is enabled inside the service database.""" + exit_code, output = exec_psql( + service.container, + user=service.user, + password=service.password, + database=service.database, + sql=f'CREATE EXTENSION IF NOT EXISTS "{extension}"', + ) + if exit_code != 0: + decoded_output = output.decode("utf-8", errors="replace") + msg = f"Failed to enable PostgreSQL extension {extension!r}: {decoded_output}" + raise RuntimeError(msg) diff --git a/tests/integration/fixtures/sqlite.py b/tests/integration/fixtures/sqlite.py index 611163864..5dd1df375 100644 --- a/tests/integration/fixtures/sqlite.py +++ b/tests/integration/fixtures/sqlite.py @@ -2,16 +2,23 @@ import sqlite3 import tempfile -from collections.abc import AsyncGenerator, Generator from contextlib import suppress from pathlib import Path -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import pytest from pytest_databases.helpers import get_xdist_worker_num -from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + + from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver +else: + AiosqliteConfig = import_optional_attr("sqlspec.adapters.aiosqlite", "AiosqliteConfig") + AiosqliteDriver = import_optional_attr("sqlspec.adapters.aiosqlite", "AiosqliteDriver") __all__ = ( "aiosqlite_config", @@ -27,13 +34,6 @@ ) -def _temporary_database_path(prefix: str) -> Path: - worker_num = get_xdist_worker_num() - worker_suffix = f"-worker-{worker_num}" if worker_num is not None else "" - with tempfile.NamedTemporaryFile(prefix=f"{prefix}{worker_suffix}-", suffix=".db", delete=False) as tmp: - return Path(tmp.name) - - @pytest.fixture def sqlite_session() -> "Generator[SqliteDriver, None, None]": """Create a SQLite session with a test table for integration tests.""" @@ -145,6 +145,7 @@ def sqlite_temp_file_config() -> "Generator[SqliteConfig, None, None]": @pytest.fixture async def aiosqlite_session_config() -> "AsyncGenerator[AiosqliteConfig, None]": """Provide a per-test config for sessions that create mutable tables.""" + pytest.importorskip("aiosqlite") config = AiosqliteConfig() try: yield config @@ -157,6 +158,7 @@ async def aiosqlite_session_config() -> "AsyncGenerator[AiosqliteConfig, None]": @pytest.fixture async def aiosqlite_session(aiosqlite_session_config: "AiosqliteConfig") -> "AsyncGenerator[AiosqliteDriver, None]": """Create an aiosqlite session with a fresh test table.""" + pytest.importorskip("aiosqlite") async with aiosqlite_session_config.provide_session() as session: await session.execute_script(""" CREATE TABLE IF NOT EXISTS test_table ( @@ -180,6 +182,7 @@ async def aiosqlite_session(aiosqlite_session_config: "AiosqliteConfig") -> "Asy @pytest.fixture(scope="session") async def aiosqlite_config() -> "AsyncGenerator[AiosqliteConfig, None]": """Provide a reusable config for tests that own their session cleanup.""" + pytest.importorskip("aiosqlite") config = AiosqliteConfig() try: yield config @@ -192,6 +195,7 @@ async def aiosqlite_config() -> "AsyncGenerator[AiosqliteConfig, None]": @pytest.fixture async def aiosqlite_config_file() -> "AsyncGenerator[AiosqliteConfig, None]": """Provide an xdist-isolated file-backed config for concurrent access tests.""" + pytest.importorskip("aiosqlite") db_path = _temporary_database_path("sqlspec-aiosqlite") config = AiosqliteConfig(connection_config={"database": str(db_path), "pool_size": 5}) try: @@ -201,3 +205,10 @@ async def aiosqlite_config_file() -> "AsyncGenerator[AiosqliteConfig, None]": await config.close_pool() config.connection_instance = None db_path.unlink(missing_ok=True) + + +def _temporary_database_path(prefix: str) -> Path: + worker_num = get_xdist_worker_num() + worker_suffix = f"-worker-{worker_num}" if worker_num is not None else "" + with tempfile.NamedTemporaryFile(prefix=f"{prefix}{worker_suffix}-", suffix=".db", delete=False) as tmp: + return Path(tmp.name) From 9fd104db21c7b73c501887215e75a4ba938abd7b Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 16:38:35 +0000 Subject: [PATCH 04/35] test(mysql): consolidate adk residual suite across all mysql drivers --- .../_shared/suite_case_metadata_contract.py | 28 +- .../mysql/aiomysql/extensions/__init__.py | 1 - .../mysql/aiomysql/extensions/adk/__init__.py | 1 - .../aiomysql/extensions/adk/test_store.py | 177 --------- .../mysql/asyncmy/extensions/__init__.py | 1 - .../mysql/asyncmy/extensions/adk/__init__.py | 1 - .../asyncmy/extensions/adk/test_store.py | 176 --------- .../mysqlconnector/extensions/__init__.py | 1 - .../mysqlconnector/extensions/adk/__init__.py | 1 - .../extensions/adk/test_store.py | 197 ---------- .../mysql/pymysql/extensions/__init__.py | 1 - .../adapters/mysql/test_adk_residuals.py | 159 ++++++++ tests/integration/fixtures/aiomysql_adk.py | 106 ----- tests/integration/fixtures/asyncmy_adk.py | 105 ----- tests/integration/fixtures/mysql.py | 369 +++++++++++++++--- .../fixtures/mysqlconnector_adk.py | 89 ----- 16 files changed, 488 insertions(+), 925 deletions(-) delete mode 100644 tests/integration/adapters/mysql/aiomysql/extensions/__init__.py delete mode 100644 tests/integration/adapters/mysql/aiomysql/extensions/adk/__init__.py delete mode 100644 tests/integration/adapters/mysql/aiomysql/extensions/adk/test_store.py delete mode 100644 tests/integration/adapters/mysql/asyncmy/extensions/__init__.py delete mode 100644 tests/integration/adapters/mysql/asyncmy/extensions/adk/__init__.py delete mode 100644 tests/integration/adapters/mysql/asyncmy/extensions/adk/test_store.py delete mode 100644 tests/integration/adapters/mysql/mysqlconnector/extensions/__init__.py delete mode 100644 tests/integration/adapters/mysql/mysqlconnector/extensions/adk/__init__.py delete mode 100644 tests/integration/adapters/mysql/mysqlconnector/extensions/adk/test_store.py delete mode 100644 tests/integration/adapters/mysql/pymysql/extensions/__init__.py create mode 100644 tests/integration/adapters/mysql/test_adk_residuals.py delete mode 100644 tests/integration/fixtures/aiomysql_adk.py delete mode 100644 tests/integration/fixtures/asyncmy_adk.py delete mode 100644 tests/integration/fixtures/mysqlconnector_adk.py diff --git a/tests/integration/adapters/_shared/suite_case_metadata_contract.py b/tests/integration/adapters/_shared/suite_case_metadata_contract.py index 744b2a21d..83ffa6c6f 100644 --- a/tests/integration/adapters/_shared/suite_case_metadata_contract.py +++ b/tests/integration/adapters/_shared/suite_case_metadata_contract.py @@ -115,6 +115,9 @@ "bigquery/test_exceptions.py", "duckdb/test_migrations.py", "duckdb/test_vector_functions.py", + "mysql/aiomysql/extensions/adk/test_store.py", + "mysql/asyncmy/extensions/adk/test_store.py", + "mysql/mysqlconnector/extensions/adk/test_store.py", "mysqlconnector/test_driver_async.py", "oracledb/extensions/events/test_queue_backend.py", "oracledb/extensions/litestar/test_store_async.py", @@ -138,18 +141,6 @@ } -def _driver_cases(params: tuple[ParameterSet, ...]) -> tuple[DriverCase, ...]: - return tuple(cast("DriverCase", param.values[0]) for param in params) - - -def _adk_cases(params: tuple[ParameterSet, ...]) -> tuple[AdkStoreCase, ...]: - return tuple(cast("AdkStoreCase", param.values[0]) for param in params) - - -def _parameter_style_cases(params: tuple[ParameterSet, ...]) -> tuple[ParameterStyleCase, ...]: - return tuple(cast("ParameterStyleCase", param.values[0]) for param in params) - - def test_active_driver_cases_do_not_use_string_deviations() -> None: """Active cases use typed capability metadata instead of broad string deviations.""" cases_with_deviations = {case.id: case.deviations for case in ACTIVE_DRIVER_CASES if case.deviations} @@ -273,6 +264,7 @@ def test_postgres_extension_cases_are_contract_owned() -> None: def test_contract_owned_adapter_local_test_files_do_not_reappear() -> None: """Adapter-local files removed by consolidation stay contract-owned.""" + assert all(not path.startswith("tests/") for path in CONTRACT_OWNED_REMOVED_TEST_PATHS) adapters_root = Path(__file__).resolve().parents[1] existing = sorted(path for path in CONTRACT_OWNED_REMOVED_TEST_PATHS if (adapters_root / path).exists()) assert existing == [] @@ -294,3 +286,15 @@ def test_parameter_style_params_are_method_filtered() -> None: assert {case.id for case in execute_cases} | {case.id for case in execute_many_cases} == { case.id for case in all_cases } + + +def _driver_cases(params: tuple[ParameterSet, ...]) -> tuple[DriverCase, ...]: + return tuple(cast("DriverCase", param.values[0]) for param in params) + + +def _adk_cases(params: tuple[ParameterSet, ...]) -> tuple[AdkStoreCase, ...]: + return tuple(cast("AdkStoreCase", param.values[0]) for param in params) + + +def _parameter_style_cases(params: tuple[ParameterSet, ...]) -> tuple[ParameterStyleCase, ...]: + return tuple(cast("ParameterStyleCase", param.values[0]) for param in params) diff --git a/tests/integration/adapters/mysql/aiomysql/extensions/__init__.py b/tests/integration/adapters/mysql/aiomysql/extensions/__init__.py deleted file mode 100644 index 840a3729f..000000000 --- a/tests/integration/adapters/mysql/aiomysql/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""aiomysql extensions integration tests.""" diff --git a/tests/integration/adapters/mysql/aiomysql/extensions/adk/__init__.py b/tests/integration/adapters/mysql/aiomysql/extensions/adk/__init__.py deleted file mode 100644 index 990cd80c7..000000000 --- a/tests/integration/adapters/mysql/aiomysql/extensions/adk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for aiomysql ADK store.""" diff --git a/tests/integration/adapters/mysql/aiomysql/extensions/adk/test_store.py b/tests/integration/adapters/mysql/aiomysql/extensions/adk/test_store.py deleted file mode 100644 index ec7ccf117..000000000 --- a/tests/integration/adapters/mysql/aiomysql/extensions/adk/test_store.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Integration tests for aiomysql ADK session store. - -The shared session/event CRUD lifecycle (create_tables, session round-trip, list/delete, -append/get events, get_events filtering) is covered by -tests/integration/adapters/_shared/suite_adk_store_contract.py. This module keeps the -adapter-specific coverage (owner_id_column, storage-type fidelity, timestamp precision, -concurrency, event ordering/JSON details) that is not portable across the contract matrix. -""" - -from datetime import UTC, datetime - -import pytest - -from sqlspec.adapters.aiomysql._typing import AiomysqlCursor -from sqlspec.adapters.aiomysql.adk import AiomysqlADKStore -from sqlspec.extensions.adk import StoredEvent - -pytestmark = [pytest.mark.xdist_group("mysql"), pytest.mark.aiomysql, pytest.mark.integration] - - -async def test_storage_types_verification(aiomysql_adk_store: AiomysqlADKStore) -> None: - """Verify MySQL uses JSON type (not TEXT) and TIMESTAMP(6) for microseconds. - - Critical verification from ADK implementation review. - Ensures we're using MySQL native types optimally. - """ - config = aiomysql_adk_store.config - - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_sessions' - ORDER BY ORDINAL_POSITION - """) - session_columns = await cursor.fetchall() - - state_col = next(col for col in session_columns if col[0] == "state") - assert state_col[1] == "json", "state column must use native JSON type (not TEXT)" - - create_time_col = next(col for col in session_columns if col[0] == "create_time") - assert "timestamp(6)" in create_time_col[2].lower(), "create_time must be TIMESTAMP(6) for microseconds" - - update_time_col = next(col for col in session_columns if col[0] == "update_time") - assert "timestamp(6)" in update_time_col[2].lower(), "update_time must be TIMESTAMP(6) for microseconds" - - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_events' - ORDER BY ORDINAL_POSITION - """) - event_columns = await cursor.fetchall() - event_col_names = [col[0] for col in event_columns] - - assert "session_id" in event_col_names - assert "invocation_id" in event_col_names - assert "timestamp" in event_col_names - assert "event_data" in event_col_names - - timestamp_col = next(col for col in event_columns if col[0] == "timestamp") - assert "timestamp(6)" in timestamp_col[2].lower(), "timestamp must be TIMESTAMP(6) for microseconds" - - -async def test_timestamp_precision(aiomysql_adk_store: AiomysqlADKStore) -> None: - """Test TIMESTAMP(6) provides microsecond precision.""" - session_id = "session-006" - app_name = "test-app" - user_id = "user-006" - - created = await aiomysql_adk_store.create_session(session_id, app_name, user_id, {"test": "precision"}) - - assert created["create_time"].microsecond > 0 or created["create_time"].microsecond == 0 - assert hasattr(created["create_time"], "microsecond") - - event_time = datetime.now(UTC) - event: StoredEvent = { - "id": "event-micro", - "app_name": app_name, - "user_id": user_id, - "session_id": session_id, - "invocation_id": "inv-micro", - "timestamp": event_time, - "event_data": {"app_name": app_name, "author": "system"}, - } - await aiomysql_adk_store.append_event(event) - - events = await aiomysql_adk_store.get_events(app_name, user_id, session_id) - assert len(events) == 1 - assert hasattr(events[0]["timestamp"], "microsecond") - - -async def test_owner_id_column_creation(aiomysql_adk_store_with_fk: AiomysqlADKStore) -> None: - """Test owner ID column is created correctly.""" - assert aiomysql_adk_store_with_fk.owner_id_column_name == "tenant_id" - assert aiomysql_adk_store_with_fk.owner_id_column_ddl is not None - assert "tenant_id" in aiomysql_adk_store_with_fk.owner_id_column_ddl - - config = aiomysql_adk_store_with_fk.config - - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_fk_sessions' - AND COLUMN_NAME = 'tenant_id' - """) - result = await cursor.fetchone() - assert result is not None - assert result[0] == "tenant_id" - assert result[1] == "bigint" - - -async def test_owner_id_constraint_enforcement(aiomysql_adk_store_with_fk: AiomysqlADKStore) -> None: - """Test FK constraint enforces referential integrity.""" - session_id = "session-fk-001" - app_name = "test-app" - user_id = "user-fk" - - await aiomysql_adk_store_with_fk.create_session( - session_id=session_id, app_name=app_name, user_id=user_id, state={"tenant": "one"}, owner_id=1 - ) - - session = await aiomysql_adk_store_with_fk.get_session(app_name, user_id, session_id) - assert session is not None - - with pytest.raises(Exception): - await aiomysql_adk_store_with_fk.create_session( - session_id="invalid-fk", app_name=app_name, user_id=user_id, state={"tenant": "invalid"}, owner_id=999 - ) - - -async def test_owner_id_cascade_delete(aiomysql_adk_store_with_fk: AiomysqlADKStore) -> None: - """Test CASCADE DELETE when parent tenant is deleted.""" - config = aiomysql_adk_store_with_fk.config - - await aiomysql_adk_store_with_fk.create_session( - session_id="tenant1-session", app_name="test-app", user_id="user1", state={"data": "test"}, owner_id=1 - ) - - session_before = await aiomysql_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_before is not None - - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute("DELETE FROM test_tenants WHERE id = 1") - await conn.commit() - - session_after = await aiomysql_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_after is None - - -async def test_multi_tenant_isolation(aiomysql_adk_store_with_fk: AiomysqlADKStore) -> None: - """Test FK column enables multi-tenant data isolation.""" - app_name = "test-app" - user_id = "user-shared" - - await aiomysql_adk_store_with_fk.create_session("tenant1-s1", app_name, user_id, {"tenant": "one"}, owner_id=1) - await aiomysql_adk_store_with_fk.create_session("tenant1-s2", app_name, user_id, {"tenant": "one"}, owner_id=1) - await aiomysql_adk_store_with_fk.create_session("tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2) - - config = aiomysql_adk_store_with_fk.config - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute( - f"SELECT id FROM {aiomysql_adk_store_with_fk.session_table} WHERE tenant_id = %s ORDER BY id", (1,) - ) - tenant1_sessions = await cursor.fetchall() - assert len(tenant1_sessions) == 2 - assert tenant1_sessions[0][0] == "tenant1-s1" - assert tenant1_sessions[1][0] == "tenant1-s2" - - await cursor.execute(f"SELECT id FROM {aiomysql_adk_store_with_fk.session_table} WHERE tenant_id = %s", (2,)) - tenant2_sessions = await cursor.fetchall() - assert len(tenant2_sessions) == 1 - assert tenant2_sessions[0][0] == "tenant2-s1" diff --git a/tests/integration/adapters/mysql/asyncmy/extensions/__init__.py b/tests/integration/adapters/mysql/asyncmy/extensions/__init__.py deleted file mode 100644 index 508f84515..000000000 --- a/tests/integration/adapters/mysql/asyncmy/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""AsyncMy extensions integration tests.""" diff --git a/tests/integration/adapters/mysql/asyncmy/extensions/adk/__init__.py b/tests/integration/adapters/mysql/asyncmy/extensions/adk/__init__.py deleted file mode 100644 index 57cf9e143..000000000 --- a/tests/integration/adapters/mysql/asyncmy/extensions/adk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for AsyncMY ADK store.""" diff --git a/tests/integration/adapters/mysql/asyncmy/extensions/adk/test_store.py b/tests/integration/adapters/mysql/asyncmy/extensions/adk/test_store.py deleted file mode 100644 index 727941e99..000000000 --- a/tests/integration/adapters/mysql/asyncmy/extensions/adk/test_store.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Integration tests for AsyncMY ADK session store. - -The shared session/event CRUD lifecycle (create_tables, session round-trip, list/delete, -append/get events, get_events filtering) is covered by -tests/integration/adapters/_shared/suite_adk_store_contract.py. This module keeps the -adapter-specific coverage (owner_id_column, storage-type fidelity, timestamp precision, -concurrency, event ordering/JSON details) that is not portable across the contract matrix. -""" - -from datetime import UTC, datetime - -import pytest - -from sqlspec.adapters.asyncmy.adk import AsyncmyADKStore -from sqlspec.extensions.adk import StoredEvent - -pytestmark = [pytest.mark.xdist_group("mysql"), pytest.mark.asyncmy, pytest.mark.integration] - - -async def test_storage_types_verification(asyncmy_adk_store: AsyncmyADKStore) -> None: - """Verify MySQL uses JSON type (not TEXT) and TIMESTAMP(6) for microseconds. - - Critical verification from ADK implementation review. - Ensures we're using MySQL native types optimally. - """ - config = asyncmy_adk_store.config - - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_sessions' - ORDER BY ORDINAL_POSITION - """) - session_columns = await cursor.fetchall() - - state_col = next(col for col in session_columns if col[0] == "state") - assert state_col[1] == "json", "state column must use native JSON type (not TEXT)" - - create_time_col = next(col for col in session_columns if col[0] == "create_time") - assert "timestamp(6)" in create_time_col[2].lower(), "create_time must be TIMESTAMP(6) for microseconds" - - update_time_col = next(col for col in session_columns if col[0] == "update_time") - assert "timestamp(6)" in update_time_col[2].lower(), "update_time must be TIMESTAMP(6) for microseconds" - - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_events' - ORDER BY ORDINAL_POSITION - """) - event_columns = await cursor.fetchall() - event_col_names = [col[0] for col in event_columns] - - assert "session_id" in event_col_names - assert "invocation_id" in event_col_names - assert "timestamp" in event_col_names - assert "event_data" in event_col_names - - timestamp_col = next(col for col in event_columns if col[0] == "timestamp") - assert "timestamp(6)" in timestamp_col[2].lower(), "timestamp must be TIMESTAMP(6) for microseconds" - - -async def test_timestamp_precision(asyncmy_adk_store: AsyncmyADKStore) -> None: - """Test TIMESTAMP(6) provides microsecond precision.""" - session_id = "session-006" - app_name = "test-app" - user_id = "user-006" - - created = await asyncmy_adk_store.create_session(session_id, app_name, user_id, {"test": "precision"}) - - assert created["create_time"].microsecond > 0 or created["create_time"].microsecond == 0 - assert hasattr(created["create_time"], "microsecond") - - event_time = datetime.now(UTC) - event: StoredEvent = { - "id": "event-micro", - "app_name": app_name, - "user_id": user_id, - "session_id": session_id, - "invocation_id": "inv-micro", - "timestamp": event_time, - "event_data": {"app_name": app_name, "author": "system"}, - } - await asyncmy_adk_store.append_event(event) - - events = await asyncmy_adk_store.get_events(app_name, user_id, session_id) - assert len(events) == 1 - assert hasattr(events[0]["timestamp"], "microsecond") - - -async def test_owner_id_column_creation(asyncmy_adk_store_with_fk: AsyncmyADKStore) -> None: - """Test owner ID column is created correctly.""" - assert asyncmy_adk_store_with_fk.owner_id_column_name == "tenant_id" - assert asyncmy_adk_store_with_fk.owner_id_column_ddl is not None - assert "tenant_id" in asyncmy_adk_store_with_fk.owner_id_column_ddl - - config = asyncmy_adk_store_with_fk.config - - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_fk_sessions' - AND COLUMN_NAME = 'tenant_id' - """) - result = await cursor.fetchone() - assert result is not None - assert result[0] == "tenant_id" - assert result[1] == "bigint" - - -async def test_owner_id_constraint_enforcement(asyncmy_adk_store_with_fk: AsyncmyADKStore) -> None: - """Test FK constraint enforces referential integrity.""" - session_id = "session-fk-001" - app_name = "test-app" - user_id = "user-fk" - - await asyncmy_adk_store_with_fk.create_session( - session_id=session_id, app_name=app_name, user_id=user_id, state={"tenant": "one"}, owner_id=1 - ) - - session = await asyncmy_adk_store_with_fk.get_session(app_name, user_id, session_id) - assert session is not None - - with pytest.raises(Exception): - await asyncmy_adk_store_with_fk.create_session( - session_id="invalid-fk", app_name=app_name, user_id=user_id, state={"tenant": "invalid"}, owner_id=999 - ) - - -async def test_owner_id_cascade_delete(asyncmy_adk_store_with_fk: AsyncmyADKStore) -> None: - """Test CASCADE DELETE when parent tenant is deleted.""" - config = asyncmy_adk_store_with_fk.config - - await asyncmy_adk_store_with_fk.create_session( - session_id="tenant1-session", app_name="test-app", user_id="user1", state={"data": "test"}, owner_id=1 - ) - - session_before = await asyncmy_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_before is not None - - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute("DELETE FROM test_tenants WHERE id = 1") - await conn.commit() - - session_after = await asyncmy_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_after is None - - -async def test_multi_tenant_isolation(asyncmy_adk_store_with_fk: AsyncmyADKStore) -> None: - """Test FK column enables multi-tenant data isolation.""" - app_name = "test-app" - user_id = "user-shared" - - await asyncmy_adk_store_with_fk.create_session("tenant1-s1", app_name, user_id, {"tenant": "one"}, owner_id=1) - await asyncmy_adk_store_with_fk.create_session("tenant1-s2", app_name, user_id, {"tenant": "one"}, owner_id=1) - await asyncmy_adk_store_with_fk.create_session("tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2) - - config = asyncmy_adk_store_with_fk.config - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute( - f"SELECT id FROM {asyncmy_adk_store_with_fk.session_table} WHERE tenant_id = %s ORDER BY id", (1,) - ) - tenant1_sessions = await cursor.fetchall() - assert len(tenant1_sessions) == 2 - assert tenant1_sessions[0][0] == "tenant1-s1" - assert tenant1_sessions[1][0] == "tenant1-s2" - - await cursor.execute(f"SELECT id FROM {asyncmy_adk_store_with_fk.session_table} WHERE tenant_id = %s", (2,)) - tenant2_sessions = await cursor.fetchall() - assert len(tenant2_sessions) == 1 - assert tenant2_sessions[0][0] == "tenant2-s1" diff --git a/tests/integration/adapters/mysql/mysqlconnector/extensions/__init__.py b/tests/integration/adapters/mysql/mysqlconnector/extensions/__init__.py deleted file mode 100644 index f03cb3664..000000000 --- a/tests/integration/adapters/mysql/mysqlconnector/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Extension tests for MysqlConnector adapter.""" diff --git a/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/__init__.py b/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/__init__.py deleted file mode 100644 index c231aeacb..000000000 --- a/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""ADK extension tests for MysqlConnector.""" diff --git a/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/test_store.py b/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/test_store.py deleted file mode 100644 index fbaddba97..000000000 --- a/tests/integration/adapters/mysql/mysqlconnector/extensions/adk/test_store.py +++ /dev/null @@ -1,197 +0,0 @@ -"""Integration tests for MysqlConnector ADK session store. - -The shared session/event CRUD lifecycle (create_tables, session round-trip, list/delete, -append/get events, get_events filtering) is covered by -tests/integration/adapters/_shared/suite_adk_store_contract.py. This module keeps the -adapter-specific coverage (owner_id_column, storage-type fidelity, timestamp precision, -concurrency, event ordering/JSON details) that is not portable across the contract matrix. -""" - -from datetime import UTC, datetime -from typing import cast - -import pytest - -from sqlspec.adapters.mysqlconnector.adk import MysqlConnectorAsyncADKStore -from sqlspec.extensions.adk import StoredEvent - -pytestmark = [pytest.mark.xdist_group("mysql"), pytest.mark.mysql_connector, pytest.mark.integration] - - -async def test_storage_types_verification(mysqlconnector_adk_store: MysqlConnectorAsyncADKStore) -> None: - """Verify MySQL uses JSON type (not TEXT) and TIMESTAMP(6) for microseconds.""" - config = mysqlconnector_adk_store.config - - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_sessions' - ORDER BY ORDINAL_POSITION - """) - session_columns = await cursor.fetchall() - - state_col = next(col for col in session_columns if col[0] == "state") - assert state_col[1] == "json", "state column must use native JSON type (not TEXT)" - - create_time_col = next(col for col in session_columns if col[0] == "create_time") - assert "timestamp(6)" in cast("str", create_time_col[2]).lower() - - update_time_col = next(col for col in session_columns if col[0] == "update_time") - assert "timestamp(6)" in cast("str", update_time_col[2]).lower() - - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_events' - ORDER BY ORDINAL_POSITION - """) - event_columns = await cursor.fetchall() - event_col_names = [col[0] for col in event_columns] - - assert "session_id" in event_col_names - assert "invocation_id" in event_col_names - assert "timestamp" in event_col_names - assert "event_data" in event_col_names - - timestamp_col = next(col for col in event_columns if col[0] == "timestamp") - assert "timestamp(6)" in cast("str", timestamp_col[2]).lower() - finally: - await cursor.close() - - -async def test_timestamp_precision(mysqlconnector_adk_store: MysqlConnectorAsyncADKStore) -> None: - """Test TIMESTAMP(6) provides microsecond precision.""" - session_id = "session-006" - app_name = "test-app" - user_id = "user-006" - - created = await mysqlconnector_adk_store.create_session(session_id, app_name, user_id, {"test": "precision"}) - - assert hasattr(created["create_time"], "microsecond") - - event_time = datetime.now(UTC) - event: StoredEvent = { - "id": "event-micro", - "app_name": app_name, - "user_id": user_id, - "session_id": session_id, - "invocation_id": "inv-micro", - "timestamp": event_time, - "event_data": {"app_name": app_name, "author": "system"}, - } - await mysqlconnector_adk_store.append_event(event) - - events = await mysqlconnector_adk_store.get_events(app_name, user_id, session_id) - assert len(events) == 1 - assert hasattr(events[0]["timestamp"], "microsecond") - - -async def test_owner_id_column_creation(mysqlconnector_adk_store_with_fk: MysqlConnectorAsyncADKStore) -> None: - """Test owner ID column is created correctly.""" - assert mysqlconnector_adk_store_with_fk.owner_id_column_name == "tenant_id" - assert mysqlconnector_adk_store_with_fk.owner_id_column_ddl is not None - assert "tenant_id" in mysqlconnector_adk_store_with_fk.owner_id_column_ddl - - config = mysqlconnector_adk_store_with_fk.config - - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute(""" - SELECT COLUMN_NAME, DATA_TYPE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'test_fk_sessions' - AND COLUMN_NAME = 'tenant_id' - """) - result = await cursor.fetchone() - finally: - await cursor.close() - assert result is not None - assert result[0] == "tenant_id" - assert result[1] == "bigint" - - -async def test_owner_id_constraint_enforcement(mysqlconnector_adk_store_with_fk: MysqlConnectorAsyncADKStore) -> None: - """Test FK constraint enforces referential integrity.""" - session_id = "session-fk-001" - app_name = "test-app" - user_id = "user-fk" - - await mysqlconnector_adk_store_with_fk.create_session( - session_id=session_id, app_name=app_name, user_id=user_id, state={"tenant": "one"}, owner_id=1 - ) - - session = await mysqlconnector_adk_store_with_fk.get_session(app_name, user_id, session_id) - assert session is not None - - with pytest.raises(Exception): - await mysqlconnector_adk_store_with_fk.create_session( - session_id="invalid-fk", app_name=app_name, user_id=user_id, state={"tenant": "invalid"}, owner_id=999 - ) - - -async def test_owner_id_cascade_delete(mysqlconnector_adk_store_with_fk: MysqlConnectorAsyncADKStore) -> None: - """Test CASCADE DELETE when parent tenant is deleted.""" - config = mysqlconnector_adk_store_with_fk.config - - await mysqlconnector_adk_store_with_fk.create_session( - session_id="tenant1-session", app_name="test-app", user_id="user1", state={"data": "test"}, owner_id=1 - ) - - session_before = await mysqlconnector_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_before is not None - - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute("DELETE FROM test_tenants WHERE id = 1") - await conn.commit() - finally: - await cursor.close() - - session_after = await mysqlconnector_adk_store_with_fk.get_session("test-app", "user1", "tenant1-session") - assert session_after is None - - -async def test_multi_tenant_isolation(mysqlconnector_adk_store_with_fk: MysqlConnectorAsyncADKStore) -> None: - """Test FK column enables multi-tenant data isolation.""" - app_name = "test-app" - user_id = "user-shared" - - await mysqlconnector_adk_store_with_fk.create_session( - "tenant1-s1", app_name, user_id, {"tenant": "one"}, owner_id=1 - ) - await mysqlconnector_adk_store_with_fk.create_session( - "tenant1-s2", app_name, user_id, {"tenant": "one"}, owner_id=1 - ) - await mysqlconnector_adk_store_with_fk.create_session( - "tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2 - ) - - config = mysqlconnector_adk_store_with_fk.config - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute( - f"SELECT id FROM {mysqlconnector_adk_store_with_fk.session_table} WHERE tenant_id = %s ORDER BY id", - (1,), - ) - tenant1_sessions = await cursor.fetchall() - assert len(tenant1_sessions) == 2 - assert tenant1_sessions[0][0] == "tenant1-s1" - assert tenant1_sessions[1][0] == "tenant1-s2" - - await cursor.execute( - f"SELECT id FROM {mysqlconnector_adk_store_with_fk.session_table} WHERE tenant_id = %s", (2,) - ) - tenant2_sessions = await cursor.fetchall() - assert len(tenant2_sessions) == 1 - assert tenant2_sessions[0][0] == "tenant2-s1" - finally: - await cursor.close() diff --git a/tests/integration/adapters/mysql/pymysql/extensions/__init__.py b/tests/integration/adapters/mysql/pymysql/extensions/__init__.py deleted file mode 100644 index 3e54b38ef..000000000 --- a/tests/integration/adapters/mysql/pymysql/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# PyMySQL extensions integration tests diff --git a/tests/integration/adapters/mysql/test_adk_residuals.py b/tests/integration/adapters/mysql/test_adk_residuals.py new file mode 100644 index 000000000..3654c7972 --- /dev/null +++ b/tests/integration/adapters/mysql/test_adk_residuals.py @@ -0,0 +1,159 @@ +"""Unified MySQL ADK residual integration tests across all MySQL driver modes.""" + +from datetime import UTC, datetime +from typing import TYPE_CHECKING + +import pytest + +from sqlspec.utils.module_loader import import_optional_attr + +if TYPE_CHECKING: + from sqlspec.extensions.adk._types import StoredEvent + from tests.integration.fixtures.mysql import MysqlAdkResidualContext +else: + StoredEvent = import_optional_attr("sqlspec.extensions.adk._types", "StoredEvent") + +pytest.importorskip("google.adk") + +pytestmark = [pytest.mark.mysql, pytest.mark.integration, pytest.mark.xdist_group("mysql")] + + +async def test_mysql_adk_json_and_timestamp6_column_types( + mysql_adk_residual_context: "MysqlAdkResidualContext", +) -> None: + """Verify MySQL ADK tables use native JSON columns and TIMESTAMP(6) microsecond columns.""" + session_columns = await mysql_adk_residual_context.select_rows( + mysql_adk_residual_context.config, + """ + SELECT + COLUMN_NAME AS column_name, + DATA_TYPE AS data_type, + COLUMN_TYPE AS column_type, + DATETIME_PRECISION AS datetime_precision + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = :table_name + ORDER BY ORDINAL_POSITION + """, + table_name=mysql_adk_residual_context.session_table, + ) + by_session_col = {str(row["column_name"]): row for row in session_columns} + assert str(by_session_col["state"]["data_type"]).lower() == "json" + assert "timestamp(6)" in str(by_session_col["create_time"]["column_type"]).lower() + assert int(by_session_col["create_time"]["datetime_precision"]) == 6 + assert "timestamp(6)" in str(by_session_col["update_time"]["column_type"]).lower() + assert int(by_session_col["update_time"]["datetime_precision"]) == 6 + + event_columns = await mysql_adk_residual_context.select_rows( + mysql_adk_residual_context.config, + """ + SELECT + COLUMN_NAME AS column_name, + DATA_TYPE AS data_type, + COLUMN_TYPE AS column_type, + DATETIME_PRECISION AS datetime_precision + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = :table_name + ORDER BY ORDINAL_POSITION + """, + table_name=mysql_adk_residual_context.events_table, + ) + by_event_col = {str(row["column_name"]): row for row in event_columns} + assert {"session_id", "invocation_id", "timestamp", "event_data"}.issubset(by_event_col) + assert str(by_event_col["event_data"]["data_type"]).lower() == "json" + assert "timestamp(6)" in str(by_event_col["timestamp"]["column_type"]).lower() + assert int(by_event_col["timestamp"]["datetime_precision"]) == 6 + + +async def test_mysql_adk_microsecond_timestamp_roundtrip(mysql_adk_residual_context: "MysqlAdkResidualContext") -> None: + """Verify TIMESTAMP(6) preserves sub-second microsecond precision on sessions and events.""" + session_id = "session-micro-001" + app_name = "test-app" + user_id = "user-micro-001" + + created = await mysql_adk_residual_context.create_session( + mysql_adk_residual_context.store, session_id, app_name, user_id, {"precision": "microsecond"} + ) + assert isinstance(created["create_time"], datetime) + + event_time = datetime(2026, 1, 15, 12, 34, 56, 123456, tzinfo=UTC) + event: StoredEvent = { + "id": "event-micro-001", + "app_name": app_name, + "user_id": user_id, + "session_id": session_id, + "invocation_id": "inv-micro-001", + "timestamp": event_time, + "event_data": {"app_name": app_name, "author": "system"}, + } + await mysql_adk_residual_context.append_event(mysql_adk_residual_context.store, event) + + events = await mysql_adk_residual_context.get_events( + mysql_adk_residual_context.store, app_name, user_id, session_id + ) + assert len(events) == 1 + assert events[0]["timestamp"].microsecond == 123456 + + +async def test_mysql_adk_owner_id_fk_constraint_isolation_and_cascade( + mysql_adk_residual_context: "MysqlAdkResidualContext", +) -> None: + """Verify owner_id_column DDL, FK referential integrity, multi-tenant isolation, and ON DELETE CASCADE.""" + fk_store = mysql_adk_residual_context.fk_store + fk_config = mysql_adk_residual_context.fk_config + assert fk_store.owner_id_column_name == "tenant_id" + assert fk_store.owner_id_column_ddl is not None + assert "tenant_id" in fk_store.owner_id_column_ddl + + tenant_col_rows = await mysql_adk_residual_context.select_rows( + fk_config, + """ + SELECT COLUMN_NAME AS column_name, DATA_TYPE AS data_type + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = :table_name + AND COLUMN_NAME = 'tenant_id' + """, + table_name=mysql_adk_residual_context.fk_session_table, + ) + assert len(tenant_col_rows) == 1 + assert str(tenant_col_rows[0]["data_type"]).lower() == "bigint" + + app_name = "test-app" + user_id = "user-shared" + await mysql_adk_residual_context.create_session( + fk_store, "tenant1-s1", app_name, user_id, {"tenant": "one"}, owner_id=1 + ) + await mysql_adk_residual_context.create_session( + fk_store, "tenant1-s2", app_name, user_id, {"tenant": "one"}, owner_id=1 + ) + await mysql_adk_residual_context.create_session( + fk_store, "tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2 + ) + + with pytest.raises(Exception): + await mysql_adk_residual_context.create_session( + fk_store, "invalid-fk", app_name, user_id, {"tenant": "invalid"}, owner_id=999 + ) + + tenant1_rows = await mysql_adk_residual_context.select_rows( + fk_config, + f"SELECT id AS id FROM {mysql_adk_residual_context.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", + tenant_id=1, + ) + assert [row["id"] for row in tenant1_rows] == ["tenant1-s1", "tenant1-s2"] + + tenant2_rows = await mysql_adk_residual_context.select_rows( + fk_config, + f"SELECT id AS id FROM {mysql_adk_residual_context.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", + tenant_id=2, + ) + assert [row["id"] for row in tenant2_rows] == ["tenant2-s1"] + + await mysql_adk_residual_context.execute_sql( + fk_config, f"DELETE FROM {mysql_adk_residual_context.tenants_table} WHERE id = 1" + ) + assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant1-s1") is None + assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant1-s2") is None + assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant2-s1") is not None diff --git a/tests/integration/fixtures/aiomysql_adk.py b/tests/integration/fixtures/aiomysql_adk.py deleted file mode 100644 index 604bdc9ab..000000000 --- a/tests/integration/fixtures/aiomysql_adk.py +++ /dev/null @@ -1,106 +0,0 @@ -"""aiomysql ADK test fixtures.""" - -from collections.abc import AsyncGenerator - -import pytest -from pytest_databases.docker.mysql import MySQLService - -from sqlspec.adapters.aiomysql import AiomysqlConfig -from sqlspec.adapters.aiomysql._typing import AiomysqlCursor -from sqlspec.adapters.aiomysql.adk import AiomysqlADKStore -from tests.integration.fixtures.mysql import _mysql_connection_config - - -@pytest.fixture -async def aiomysql_adk_store(mysql_service: MySQLService) -> "AsyncGenerator[AiomysqlADKStore, None]": - """Create aiomysql ADK store with test database. - - Args: - mysql_service: Pytest fixture providing MySQL connection config. - - Yields: - Configured aiomysql ADK store instance. - - Notes: - Uses pytest-databases MySQL container for testing. - Tables are created before test and cleaned up after. - """ - connection_config = _mysql_connection_config(mysql_service, database_key="db") - connection_config.update({"autocommit": False, "minsize": 1, "maxsize": 5}) - config = AiomysqlConfig( - connection_config=connection_config, - extension_config={"adk": {"session_table": "test_sessions", "events_table": "test_events"}}, - ) - - try: - store = AiomysqlADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute("DROP TABLE IF EXISTS test_events") - await cursor.execute("DROP TABLE IF EXISTS test_sessions") - await conn.commit() - finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None - - -@pytest.fixture -async def aiomysql_adk_store_with_fk(mysql_service: MySQLService) -> "AsyncGenerator[AiomysqlADKStore, None]": - """Create aiomysql ADK store with owner ID column. - - Args: - mysql_service: Pytest fixture providing MySQL connection config. - - Yields: - Configured aiomysql ADK store with FK column. - - Notes: - Creates a tenants table and configures FK constraint. - Tests multi-tenant isolation and CASCADE behavior. - """ - connection_config = _mysql_connection_config(mysql_service, database_key="db") - connection_config.update({"autocommit": False, "minsize": 1, "maxsize": 5}) - config = AiomysqlConfig( - connection_config=connection_config, - extension_config={ - "adk": { - "session_table": "test_fk_sessions", - "events_table": "test_fk_events", - "owner_id_column": "tenant_id BIGINT NOT NULL REFERENCES test_tenants(id) ON DELETE CASCADE", - } - }, - ) - - try: - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute(""" - CREATE TABLE IF NOT EXISTS test_tenants ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(128) NOT NULL UNIQUE - ) ENGINE=InnoDB - """) - await cursor.execute("INSERT INTO test_tenants (name) VALUES ('tenant1'), ('tenant2')") - await conn.commit() - - store = AiomysqlADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn, AiomysqlCursor(conn) as cursor: - await cursor.execute("DROP TABLE IF EXISTS test_fk_events") - await cursor.execute("DROP TABLE IF EXISTS test_fk_sessions") - await cursor.execute("DROP TABLE IF EXISTS test_tenants") - await conn.commit() - finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None diff --git a/tests/integration/fixtures/asyncmy_adk.py b/tests/integration/fixtures/asyncmy_adk.py deleted file mode 100644 index c5ebc9f58..000000000 --- a/tests/integration/fixtures/asyncmy_adk.py +++ /dev/null @@ -1,105 +0,0 @@ -"""AsyncMY ADK test fixtures.""" - -from collections.abc import AsyncGenerator - -import pytest -from pytest_databases.docker.mysql import MySQLService - -from sqlspec.adapters.asyncmy import AsyncmyConfig -from sqlspec.adapters.asyncmy.adk import AsyncmyADKStore -from tests.integration.fixtures.mysql import _mysql_connection_config - - -@pytest.fixture -async def asyncmy_adk_store(mysql_service: MySQLService) -> "AsyncGenerator[AsyncmyADKStore, None]": - """Create AsyncMY ADK store with test database. - - Args: - mysql_service: Pytest fixture providing MySQL connection config. - - Yields: - Configured AsyncMY ADK store instance. - - Notes: - Uses pytest-databases MySQL container for testing. - Tables are created before test and cleaned up after. - """ - connection_config = _mysql_connection_config(mysql_service) - connection_config.update({"autocommit": False, "minsize": 1, "maxsize": 5}) - config = AsyncmyConfig( - connection_config=connection_config, - extension_config={"adk": {"session_table": "test_sessions", "events_table": "test_events"}}, - ) - - try: - store = AsyncmyADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute("DROP TABLE IF EXISTS test_events") - await cursor.execute("DROP TABLE IF EXISTS test_sessions") - await conn.commit() - finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None - - -@pytest.fixture -async def asyncmy_adk_store_with_fk(mysql_service: MySQLService) -> "AsyncGenerator[AsyncmyADKStore, None]": - """Create AsyncMY ADK store with owner ID column. - - Args: - mysql_service: Pytest fixture providing MySQL connection config. - - Yields: - Configured AsyncMY ADK store with FK column. - - Notes: - Creates a tenants table and configures FK constraint. - Tests multi-tenant isolation and CASCADE behavior. - """ - connection_config = _mysql_connection_config(mysql_service) - connection_config.update({"autocommit": False, "minsize": 1, "maxsize": 5}) - config = AsyncmyConfig( - connection_config=connection_config, - extension_config={ - "adk": { - "session_table": "test_fk_sessions", - "events_table": "test_fk_events", - "owner_id_column": "tenant_id BIGINT NOT NULL REFERENCES test_tenants(id) ON DELETE CASCADE", - } - }, - ) - - try: - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute(""" - CREATE TABLE IF NOT EXISTS test_tenants ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(128) NOT NULL UNIQUE - ) ENGINE=InnoDB - """) - await cursor.execute("INSERT INTO test_tenants (name) VALUES ('tenant1'), ('tenant2')") - await conn.commit() - - store = AsyncmyADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn, conn.cursor() as cursor: - await cursor.execute("DROP TABLE IF EXISTS test_fk_events") - await cursor.execute("DROP TABLE IF EXISTS test_fk_sessions") - await cursor.execute("DROP TABLE IF EXISTS test_tenants") - await conn.commit() - finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None diff --git a/tests/integration/fixtures/mysql.py b/tests/integration/fixtures/mysql.py index f01bcfa12..1c2ef582c 100644 --- a/tests/integration/fixtures/mysql.py +++ b/tests/integration/fixtures/mysql.py @@ -1,33 +1,74 @@ """Shared MySQL-family integration fixtures.""" -from collections.abc import AsyncGenerator, Generator -from typing import Any +import contextlib +from typing import TYPE_CHECKING, Any, cast +from uuid import uuid4 +import msgspec import pytest -from pytest_databases.docker.mysql import MySQLService - -from sqlspec.adapters.aiomysql import AiomysqlConfig, AiomysqlDriver -from sqlspec.adapters.aiomysql import default_statement_config as aiomysql_statement_config -from sqlspec.adapters.asyncmy import AsyncmyConfig, AsyncmyDriver -from sqlspec.adapters.asyncmy import default_statement_config as asyncmy_statement_config -from sqlspec.adapters.mysqlconnector import ( - MysqlConnectorAsyncConfig, - MysqlConnectorAsyncDriver, - MysqlConnectorSyncConfig, - MysqlConnectorSyncDriver, -) -from sqlspec.adapters.mysqlconnector import default_statement_config as mysqlconnector_statement_config -from sqlspec.adapters.pymysql import PyMysqlConfig, PyMysqlDriver -from sqlspec.adapters.pymysql import default_statement_config as pymysql_statement_config + +from sqlspec.utils.module_loader import import_optional_attr from tests.integration.fixtures.cleanup import mysql_cleanup_statements +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + + from pytest_databases.docker.mysql import MySQLService + + from sqlspec.adapters.aiomysql import AiomysqlConfig, AiomysqlDriver + from sqlspec.adapters.aiomysql import default_statement_config as aiomysql_statement_config + from sqlspec.adapters.aiomysql.adk import AiomysqlADKStore + from sqlspec.adapters.asyncmy import AsyncmyConfig, AsyncmyDriver + from sqlspec.adapters.asyncmy import default_statement_config as asyncmy_statement_config + from sqlspec.adapters.asyncmy.adk import AsyncmyADKStore + from sqlspec.adapters.mysqlconnector import ( + MysqlConnectorAsyncConfig, + MysqlConnectorAsyncDriver, + MysqlConnectorSyncConfig, + MysqlConnectorSyncDriver, + ) + from sqlspec.adapters.mysqlconnector import default_statement_config as mysqlconnector_statement_config + from sqlspec.adapters.mysqlconnector.adk import MysqlConnectorAsyncADKStore, MysqlConnectorSyncADKStore + from sqlspec.adapters.pymysql import PyMysqlConfig, PyMysqlDriver + from sqlspec.adapters.pymysql import default_statement_config as pymysql_statement_config + from sqlspec.adapters.pymysql.adk import PyMysqlADKStore + from sqlspec.extensions.adk._types import StoredEvent, StoredSession +else: + AiomysqlConfig = import_optional_attr("sqlspec.adapters.aiomysql", "AiomysqlConfig") + AiomysqlDriver = import_optional_attr("sqlspec.adapters.aiomysql", "AiomysqlDriver") + aiomysql_statement_config = import_optional_attr("sqlspec.adapters.aiomysql", "default_statement_config") + AiomysqlADKStore = import_optional_attr("sqlspec.adapters.aiomysql.adk", "AiomysqlADKStore") + AsyncmyConfig = import_optional_attr("sqlspec.adapters.asyncmy", "AsyncmyConfig") + AsyncmyDriver = import_optional_attr("sqlspec.adapters.asyncmy", "AsyncmyDriver") + asyncmy_statement_config = import_optional_attr("sqlspec.adapters.asyncmy", "default_statement_config") + AsyncmyADKStore = import_optional_attr("sqlspec.adapters.asyncmy.adk", "AsyncmyADKStore") + MysqlConnectorAsyncConfig = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorAsyncConfig") + MysqlConnectorAsyncDriver = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorAsyncDriver") + MysqlConnectorSyncConfig = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorSyncConfig") + MysqlConnectorSyncDriver = import_optional_attr("sqlspec.adapters.mysqlconnector", "MysqlConnectorSyncDriver") + mysqlconnector_statement_config = import_optional_attr( + "sqlspec.adapters.mysqlconnector", "default_statement_config" + ) + MysqlConnectorAsyncADKStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.adk", "MysqlConnectorAsyncADKStore" + ) + MysqlConnectorSyncADKStore = import_optional_attr( + "sqlspec.adapters.mysqlconnector.adk", "MysqlConnectorSyncADKStore" + ) + PyMysqlConfig = import_optional_attr("sqlspec.adapters.pymysql", "PyMysqlConfig") + PyMysqlDriver = import_optional_attr("sqlspec.adapters.pymysql", "PyMysqlDriver") + pymysql_statement_config = import_optional_attr("sqlspec.adapters.pymysql", "default_statement_config") + PyMysqlADKStore = import_optional_attr("sqlspec.adapters.pymysql.adk", "PyMysqlADKStore") + __all__ = ( + "MysqlAdkResidualContext", "aiomysql_clean_driver", "aiomysql_config", "aiomysql_driver", "asyncmy_clean_driver", "asyncmy_config", "asyncmy_driver", + "mysql_adk_residual_context", "mysqlconnector_async_config", "mysqlconnector_async_driver", "mysqlconnector_clean_async_driver", @@ -41,42 +82,90 @@ "pymysql_transaction_config", ) - -def _mysql_connection_config(mysql_service: "MySQLService", *, database_key: str = "database") -> "dict[str, Any]": - return { - "host": mysql_service.host, - "port": mysql_service.port, - "user": mysql_service.user, - "password": mysql_service.password, - database_key: mysql_service.db, - "autocommit": True, - } - - -async def _run_async_cleanup(driver: Any, adapter_suffix: str, *, procedure_suffix: str | None = None) -> None: - for statement in mysql_cleanup_statements(adapter_suffix, procedure_suffix=procedure_suffix): - await driver.execute_script(statement) - - -def _run_sync_cleanup(driver: Any, adapter_suffix: str, *, procedure_suffix: str | None = None) -> None: - for statement in mysql_cleanup_statements(adapter_suffix, procedure_suffix=procedure_suffix): - driver.execute_script(statement) +_MYSQL_ADK_DRIVER_PACKAGES: dict[str, str] = { + "aiomysql": "aiomysql", + "asyncmy": "asyncmy", + "mysqlconnector-async": "mysql.connector", + "mysqlconnector-sync": "mysql.connector", + "pymysql": "pymysql", +} + + +class MysqlAdkResidualContext(msgspec.Struct, frozen=True, gc=False): + """Unified test harness for MySQL ADK residual schema, precision, and FK checks.""" + + driver_mode: str + is_async: bool + store: Any + fk_store: Any + config: Any + fk_config: Any + session_table: str + events_table: str + fk_session_table: str + fk_events_table: str + tenants_table: str + + async def create_session( + self, + store: Any, + session_id: str, + app_name: str, + user_id: str, + state: dict[str, Any], + *, + owner_id: int | None = None, + ) -> "StoredSession": + """Create an ADK session across sync or async MySQL stores.""" + if self.is_async: + return cast( + "StoredSession", await store.create_session(session_id, app_name, user_id, state, owner_id=owner_id) + ) + return cast("StoredSession", store.create_session(session_id, app_name, user_id, state, owner_id=owner_id)) + + async def get_session(self, store: Any, app_name: str, user_id: str, session_id: str) -> "StoredSession | None": + """Fetch an ADK session across sync or async MySQL stores.""" + if self.is_async: + return cast("StoredSession | None", await store.get_session(app_name, user_id, session_id)) + return cast("StoredSession | None", store.get_session(app_name, user_id, session_id)) + + async def append_event(self, store: Any, event: "StoredEvent") -> None: + """Append an ADK event across sync or async MySQL stores.""" + if self.is_async: + await store.append_event(event) + return + store.append_event(event) + + async def get_events(self, store: Any, app_name: str, user_id: str, session_id: str) -> "list[StoredEvent]": + """Read ADK events across sync or async MySQL stores.""" + if self.is_async: + return cast("list[StoredEvent]", await store.get_events(app_name, user_id, session_id)) + return cast("list[StoredEvent]", store.get_events(app_name, user_id, session_id)) + + async def select_rows(self, config: Any, sql: str, **params: Any) -> list[dict[str, Any]]: + """Execute a SELECT statement on the given MySQL config and return dict rows.""" + if self.is_async: + async with config.provide_session() as driver: + return cast("list[dict[str, Any]]", await driver.select(sql, **params)) + with config.provide_session() as driver: + return cast("list[dict[str, Any]]", driver.select(sql, **params)) + + async def execute_sql(self, config: Any, sql: str, **params: Any) -> None: + """Execute a DML/DDL statement on the given MySQL config and commit.""" + await _execute_mysql_sql(config, sql, is_async=self.is_async, **params) @pytest.fixture(scope="session") async def asyncmy_config(mysql_service: "MySQLService") -> "AsyncGenerator[AsyncmyConfig, None]": """Provide a session-scoped AsyncmyConfig with a shared pool.""" + pytest.importorskip("asyncmy") connection_config = _mysql_connection_config(mysql_service) connection_config.update({"minsize": 1, "maxsize": 5}) config = AsyncmyConfig(connection_config=connection_config, statement_config=asyncmy_statement_config) try: yield config finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None + await config.close_pool() @pytest.fixture @@ -98,17 +187,14 @@ async def asyncmy_clean_driver(asyncmy_config: "AsyncmyConfig") -> "AsyncGenerat @pytest.fixture(scope="session") async def aiomysql_config(mysql_service: "MySQLService") -> "AsyncGenerator[AiomysqlConfig, None]": """Provide a session-scoped AiomysqlConfig with a shared pool.""" + pytest.importorskip("aiomysql") connection_config = _mysql_connection_config(mysql_service, database_key="db") connection_config.update({"minsize": 1, "maxsize": 5}) config = AiomysqlConfig(connection_config=connection_config, statement_config=aiomysql_statement_config) try: yield config finally: - pool = config.connection_instance - if pool is not None: - pool.close() - await pool.wait_closed() - config.connection_instance = None + await config.close_pool() @pytest.fixture @@ -132,6 +218,7 @@ async def mysqlconnector_async_config( mysql_service: "MySQLService", ) -> "AsyncGenerator[MysqlConnectorAsyncConfig, None]": """Provide a session-scoped MysqlConnector async configuration.""" + pytest.importorskip("mysql.connector") connection_config = _mysql_connection_config(mysql_service) connection_config["use_pure"] = True config = MysqlConnectorAsyncConfig( @@ -140,12 +227,13 @@ async def mysqlconnector_async_config( try: yield config finally: - config.connection_instance = None + await config.close_pool() @pytest.fixture(scope="session") def mysqlconnector_sync_config(mysql_service: "MySQLService") -> "Generator[MysqlConnectorSyncConfig, None, None]": """Create a MysqlConnector sync configuration for testing.""" + pytest.importorskip("mysql.connector") connection_config = _mysql_connection_config(mysql_service) connection_config.update({"use_pure": True, "pool_size": 5}) config = MysqlConnectorSyncConfig( @@ -154,8 +242,7 @@ def mysqlconnector_sync_config(mysql_service: "MySQLService") -> "Generator[Mysq try: yield config finally: - if config.connection_instance: - config.close_pool() + config.close_pool() @pytest.fixture @@ -163,6 +250,7 @@ def mysqlconnector_sync_transaction_config( mysql_service: "MySQLService", ) -> "Generator[MysqlConnectorSyncConfig, None, None]": """Create a MysqlConnector sync configuration for transaction testing.""" + pytest.importorskip("mysql.connector") connection_config = _mysql_connection_config(mysql_service) connection_config.update({"autocommit": False, "use_pure": True}) config = MysqlConnectorSyncConfig( @@ -171,8 +259,7 @@ def mysqlconnector_sync_transaction_config( try: yield config finally: - if config.connection_instance: - config.close_pool() + config.close_pool() @pytest.fixture @@ -218,27 +305,27 @@ def mysqlconnector_clean_sync_driver( @pytest.fixture(scope="session") def pymysql_config(mysql_service: "MySQLService") -> "Generator[PyMysqlConfig, None, None]": """Create a PyMySQL config for testing.""" + pytest.importorskip("pymysql") config = PyMysqlConfig( connection_config=_mysql_connection_config(mysql_service), statement_config=pymysql_statement_config ) try: yield config finally: - if config.connection_instance: - config.close_pool() + config.close_pool() @pytest.fixture def pymysql_transaction_config(mysql_service: "MySQLService") -> "Generator[PyMysqlConfig, None, None]": """Create a PyMySQL config for transaction testing.""" + pytest.importorskip("pymysql") connection_config = _mysql_connection_config(mysql_service) connection_config["autocommit"] = False config = PyMysqlConfig(connection_config=connection_config, statement_config=pymysql_statement_config) try: yield config finally: - if config.connection_instance: - config.close_pool() + config.close_pool() @pytest.fixture @@ -255,3 +342,173 @@ def pymysql_clean_driver(pymysql_config: "PyMysqlConfig") -> "Generator[PyMysqlD _run_sync_cleanup(driver, "pymysql") yield driver _run_sync_cleanup(driver, "pymysql") + + +@pytest.fixture(params=("aiomysql", "asyncmy", "mysqlconnector-async", "mysqlconnector-sync", "pymysql")) +async def mysql_adk_residual_context( + request: pytest.FixtureRequest, mysql_service: "MySQLService" +) -> "AsyncGenerator[MysqlAdkResidualContext, None]": + """Provide isolated ADK stores across all MySQL driver modes with deterministic pool teardown.""" + pytest.importorskip("google.adk") + driver_mode = str(request.param) + pytest.importorskip(_MYSQL_ADK_DRIVER_PACKAGES[driver_mode]) + + suffix = uuid4().hex[:8] + session_table = f"adk_rs_{suffix}" + events_table = f"adk_re_{suffix}" + app_state_table = f"adk_ras_{suffix}" + user_state_table = f"adk_rus_{suffix}" + metadata_table = f"adk_rm_{suffix}" + + fk_session_table = f"adk_fks_{suffix}" + fk_events_table = f"adk_fke_{suffix}" + fk_app_state_table = f"adk_fkas_{suffix}" + fk_user_state_table = f"adk_fkus_{suffix}" + fk_metadata_table = f"adk_fkm_{suffix}" + tenants_table = f"adk_ten_{suffix}" + + base_adk_config: dict[str, Any] = { + "session_table": session_table, + "events_table": events_table, + "app_state_table": app_state_table, + "user_state_table": user_state_table, + "metadata_table": metadata_table, + } + fk_adk_config: dict[str, Any] = { + "session_table": fk_session_table, + "events_table": fk_events_table, + "app_state_table": fk_app_state_table, + "user_state_table": fk_user_state_table, + "metadata_table": fk_metadata_table, + "owner_id_column": f"tenant_id BIGINT NOT NULL REFERENCES {tenants_table}(id) ON DELETE CASCADE", + } + + is_async, config, store = _build_mysql_adk_config_and_store(mysql_service, driver_mode, base_adk_config) + _, fk_config, fk_store = _build_mysql_adk_config_and_store(mysql_service, driver_mode, fk_adk_config) + + tables_to_drop = ( + events_table, + session_table, + app_state_table, + user_state_table, + metadata_table, + fk_events_table, + fk_session_table, + fk_app_state_table, + fk_user_state_table, + fk_metadata_table, + tenants_table, + ) + + try: + await _execute_mysql_sql( + fk_config, + f""" + CREATE TABLE IF NOT EXISTS {tenants_table} ( + id BIGINT PRIMARY KEY, + name VARCHAR(128) NOT NULL UNIQUE + ) ENGINE=InnoDB + """, + is_async=is_async, + ) + await _execute_mysql_sql( + fk_config, + f"INSERT INTO {tenants_table} (id, name) VALUES (1, 'tenant1'), (2, 'tenant2')", + is_async=is_async, + ) + if is_async: + await store.create_tables() + await fk_store.create_tables() + else: + store.create_tables() + fk_store.create_tables() + + yield MysqlAdkResidualContext( + driver_mode=driver_mode, + is_async=is_async, + store=store, + fk_store=fk_store, + config=config, + fk_config=fk_config, + session_table=session_table, + events_table=events_table, + fk_session_table=fk_session_table, + fk_events_table=fk_events_table, + tenants_table=tenants_table, + ) + finally: + await _close_mysql_config(fk_config, is_async=is_async) + for table_name in tables_to_drop: + with contextlib.suppress(Exception): + await _execute_mysql_sql(config, f"DROP TABLE IF EXISTS {table_name}", is_async=is_async) + await _close_mysql_config(config, is_async=is_async) + + +def _mysql_connection_config(mysql_service: "MySQLService", *, database_key: str = "database") -> "dict[str, Any]": + return { + "host": mysql_service.host, + "port": mysql_service.port, + "user": mysql_service.user, + "password": mysql_service.password, + database_key: mysql_service.db, + "autocommit": True, + } + + +async def _run_async_cleanup(driver: Any, adapter_suffix: str, *, procedure_suffix: str | None = None) -> None: + for statement in mysql_cleanup_statements(adapter_suffix, procedure_suffix=procedure_suffix): + await driver.execute_script(statement) + + +def _run_sync_cleanup(driver: Any, adapter_suffix: str, *, procedure_suffix: str | None = None) -> None: + for statement in mysql_cleanup_statements(adapter_suffix, procedure_suffix=procedure_suffix): + driver.execute_script(statement) + + +def _build_mysql_adk_config_and_store( + mysql_service: "MySQLService", driver_mode: str, adk_config: dict[str, Any] +) -> tuple[bool, Any, Any]: + if driver_mode == "aiomysql": + conn_cfg = _mysql_connection_config(mysql_service, database_key="db") + conn_cfg.update({"minsize": 1, "maxsize": 3}) + aiomysql_cfg = AiomysqlConfig(connection_config=conn_cfg, extension_config={"adk": adk_config}) + return True, aiomysql_cfg, AiomysqlADKStore(aiomysql_cfg) + if driver_mode == "asyncmy": + conn_cfg = _mysql_connection_config(mysql_service) + conn_cfg.update({"minsize": 1, "maxsize": 3}) + asyncmy_cfg = AsyncmyConfig(connection_config=conn_cfg, extension_config={"adk": adk_config}) + return True, asyncmy_cfg, AsyncmyADKStore(asyncmy_cfg) + if driver_mode == "mysqlconnector-async": + conn_cfg = _mysql_connection_config(mysql_service) + conn_cfg["use_pure"] = True + mc_async_cfg = MysqlConnectorAsyncConfig(connection_config=conn_cfg, extension_config={"adk": adk_config}) + return True, mc_async_cfg, MysqlConnectorAsyncADKStore(mc_async_cfg) + if driver_mode == "mysqlconnector-sync": + conn_cfg = _mysql_connection_config(mysql_service) + conn_cfg.update({"use_pure": True, "pool_size": 3}) + mc_sync_cfg = MysqlConnectorSyncConfig(connection_config=conn_cfg, extension_config={"adk": adk_config}) + return False, mc_sync_cfg, MysqlConnectorSyncADKStore(mc_sync_cfg) + if driver_mode == "pymysql": + conn_cfg = _mysql_connection_config(mysql_service) + pymysql_cfg = PyMysqlConfig(connection_config=conn_cfg, extension_config={"adk": adk_config}) + return False, pymysql_cfg, PyMysqlADKStore(pymysql_cfg) + msg = f"Unsupported MySQL ADK driver mode: {driver_mode}" + raise ValueError(msg) + + +async def _execute_mysql_sql(config: Any, sql: str, *, is_async: bool, **params: Any) -> None: + if is_async: + async with config.provide_session() as driver: + await driver.execute(sql, **params) + await driver.commit() + return + with config.provide_session() as driver: + driver.execute(sql, **params) + driver.commit() + + +async def _close_mysql_config(config: Any, *, is_async: bool) -> None: + if is_async: + await config.close_pool() + return + config.close_pool() diff --git a/tests/integration/fixtures/mysqlconnector_adk.py b/tests/integration/fixtures/mysqlconnector_adk.py deleted file mode 100644 index e13b216b0..000000000 --- a/tests/integration/fixtures/mysqlconnector_adk.py +++ /dev/null @@ -1,89 +0,0 @@ -"""MysqlConnector ADK test fixtures.""" - -from collections.abc import AsyncGenerator - -import pytest -from pytest_databases.docker.mysql import MySQLService - -from sqlspec.adapters.mysqlconnector import MysqlConnectorAsyncConfig -from sqlspec.adapters.mysqlconnector.adk import MysqlConnectorAsyncADKStore -from tests.integration.fixtures.mysql import _mysql_connection_config - - -@pytest.fixture -async def mysqlconnector_adk_store(mysql_service: MySQLService) -> "AsyncGenerator[MysqlConnectorAsyncADKStore, None]": - """Create MysqlConnector ADK store with test database.""" - connection_config = _mysql_connection_config(mysql_service) - connection_config.update({"autocommit": False, "use_pure": True}) - config = MysqlConnectorAsyncConfig( - connection_config=connection_config, - extension_config={"adk": {"session_table": "test_sessions", "events_table": "test_events"}}, - ) - - try: - store = MysqlConnectorAsyncADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute("DROP TABLE IF EXISTS test_events") - await cursor.execute("DROP TABLE IF EXISTS test_sessions") - await conn.commit() - finally: - await cursor.close() - finally: - config.connection_instance = None - - -@pytest.fixture -async def mysqlconnector_adk_store_with_fk( - mysql_service: MySQLService, -) -> "AsyncGenerator[MysqlConnectorAsyncADKStore, None]": - """Create MysqlConnector ADK store with owner ID column.""" - connection_config = _mysql_connection_config(mysql_service) - connection_config.update({"autocommit": False, "use_pure": True}) - config = MysqlConnectorAsyncConfig( - connection_config=connection_config, - extension_config={ - "adk": { - "session_table": "test_fk_sessions", - "events_table": "test_fk_events", - "owner_id_column": "tenant_id BIGINT NOT NULL REFERENCES test_tenants(id) ON DELETE CASCADE", - } - }, - ) - - try: - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute(""" - CREATE TABLE IF NOT EXISTS test_tenants ( - id BIGINT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(128) NOT NULL UNIQUE - ) ENGINE=InnoDB - """) - await cursor.execute("INSERT INTO test_tenants (name) VALUES ('tenant1'), ('tenant2')") - await conn.commit() - finally: - await cursor.close() - - store = MysqlConnectorAsyncADKStore(config) - await store.create_tables() - - yield store - - async with config.provide_connection() as conn: - cursor = await conn.cursor() - try: - await cursor.execute("DROP TABLE IF EXISTS test_fk_events") - await cursor.execute("DROP TABLE IF EXISTS test_fk_sessions") - await cursor.execute("DROP TABLE IF EXISTS test_tenants") - await conn.commit() - finally: - await cursor.close() - finally: - config.connection_instance = None From 77a2ded30ad4368c947a9db43ccb9a51c55b659b Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 17:07:27 +0000 Subject: [PATCH 05/35] test(storage): provision rustfs bucket via stdlib sigv4 and decouple s3fs teardown --- tests/fixtures/rustfs.py | 75 ++++++++++++++ .../test_native_storage_capabilities.py | 18 ++-- .../duckdb/duckdb/test_native_storage.py | 87 +++++++++------- .../sqlite/extensions/adk/test_maintenance.py | 42 ++++---- .../extensions/adk/test_memory_store.py | 42 ++++---- .../extensions/adk/test_owner_id_column.py | 98 +++++++++---------- tests/integration/conftest.py | 4 +- tests/integration/fixtures/bigquery.py | 36 +++---- 8 files changed, 245 insertions(+), 157 deletions(-) diff --git a/tests/fixtures/rustfs.py b/tests/fixtures/rustfs.py index 39e70d972..9216b21fd 100644 --- a/tests/fixtures/rustfs.py +++ b/tests/fixtures/rustfs.py @@ -1,6 +1,12 @@ """RustFS-backed S3-compatible test helpers.""" +import hashlib +import hmac +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any +from urllib.error import HTTPError +from urllib.parse import urlsplit +from urllib.request import Request, urlopen import pytest @@ -8,6 +14,8 @@ from pytest_databases.docker.rustfs import RustfsService __all__ = ( + "ensure_rustfs_bucket", + "ensure_s3_bucket", "rustfs_endpoint_url", "rustfs_filesystem", "rustfs_fsspec_kwargs", @@ -16,6 +24,65 @@ ) +def ensure_s3_bucket( + endpoint_url: str, bucket_name: str, *, access_key: str, secret_key: str, region: str = "us-east-1" +) -> str: + """Create an S3 bucket idempotently via stdlib AWS SigV4 HTTP PUT without third-party SDKs.""" + parsed = urlsplit(endpoint_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + msg = f"S3 endpoint_url must be a valid http(s) URL, got {endpoint_url!r}" + raise ValueError(msg) + + now = datetime.now(UTC) + amz_date = now.strftime("%Y%m%dT%H%M%SZ") + date_stamp = now.strftime("%Y%m%d") + payload_hash = hashlib.sha256(b"").hexdigest() + canonical_uri = f"/{bucket_name}" + canonical_headers = f"host:{parsed.netloc}\nx-amz-content-sha256:{payload_hash}\nx-amz-date:{amz_date}\n" + signed_headers = "host;x-amz-content-sha256;x-amz-date" + canonical_request = f"PUT\n{canonical_uri}\n\n{canonical_headers}\n{signed_headers}\n{payload_hash}" + credential_scope = f"{date_stamp}/{region}/s3/aws4_request" + request_digest = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest() + string_to_sign = f"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{request_digest}" + signing_key = _derive_aws_v4_signing_key(secret_key, date_stamp=date_stamp, region=region, service="s3") + signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() + authorization = ( + f"AWS4-HMAC-SHA256 Credential={access_key}/{credential_scope}, " + f"SignedHeaders={signed_headers}, Signature={signature}" + ) + bucket_url = f"{parsed.scheme}://{parsed.netloc}{canonical_uri}" + request = Request( + bucket_url, + data=None, + method="PUT", + headers={ + "Authorization": authorization, + "Content-Length": "0", + "x-amz-content-sha256": payload_hash, + "x-amz-date": amz_date, + }, + ) + try: + with urlopen(request, timeout=10) as response: + _ = response.read() + except HTTPError as exc: + body = exc.read().decode("utf-8", errors="replace") + if exc.code == 409 and ("BucketAlreadyOwnedByYou" in body or "BucketAlreadyExists" in body): + return bucket_name + raise + return bucket_name + + +def ensure_rustfs_bucket(rustfs_service: "RustfsService", bucket_name: str) -> str: + """Ensure a bucket exists on a RustFS service without requiring s3fs or obstore.""" + return ensure_s3_bucket( + rustfs_endpoint_url(rustfs_service), + bucket_name, + access_key=rustfs_service.access_key, + secret_key=rustfs_service.secret_key, + ) + + def rustfs_endpoint_url(rustfs_service: "RustfsService") -> str: """Return the S3 endpoint URL for a pytest-databases RustFS service.""" scheme = "https" if rustfs_service.secure else "http" @@ -60,3 +127,11 @@ def rustfs_object_size(rustfs_service: "RustfsService", bucket_name: str, object if isinstance(size, int): return size return 0 + + +def _derive_aws_v4_signing_key(secret_key: str, *, date_stamp: str, region: str, service: str) -> bytes: + """Derive an AWS Signature Version 4 signing key.""" + key_date = hmac.new(f"AWS4{secret_key}".encode(), date_stamp.encode("utf-8"), hashlib.sha256).digest() + key_region = hmac.new(key_date, region.encode("utf-8"), hashlib.sha256).digest() + key_service = hmac.new(key_region, service.encode("utf-8"), hashlib.sha256).digest() + return hmac.new(key_service, b"aws4_request", hashlib.sha256).digest() diff --git a/tests/integration/adapters/cockroach/test_native_storage_capabilities.py b/tests/integration/adapters/cockroach/test_native_storage_capabilities.py index bda1e7fbc..0a29d70b5 100644 --- a/tests/integration/adapters/cockroach/test_native_storage_capabilities.py +++ b/tests/integration/adapters/cockroach/test_native_storage_capabilities.py @@ -1,5 +1,6 @@ """Local server contracts required by CockroachDB native object storage.""" +from importlib.util import find_spec from typing import TYPE_CHECKING, Literal from urllib.parse import urlencode, urlsplit, urlunsplit from uuid import uuid4 @@ -48,14 +49,10 @@ def native_storage_uri( try: yield f"s3://{rustfs_bucket_name}/{prefix}?{query}" finally: - filesystem = rustfs_filesystem(rustfs_service) - if filesystem.exists(f"{rustfs_bucket_name}/{prefix}"): - filesystem.rm(f"{rustfs_bucket_name}/{prefix}", recursive=True) - - -def _file_uri(destination: str, filename: str) -> str: - parts = urlsplit(destination) - return urlunsplit(parts._replace(path=f"{parts.path}/{filename}")) + if find_spec("s3fs") is not None and find_spec("fsspec") is not None: + filesystem = rustfs_filesystem(rustfs_service) + if filesystem.exists(f"{rustfs_bucket_name}/{prefix}"): + filesystem.rm(f"{rustfs_bucket_name}/{prefix}", recursive=True) def test_native_storage_connection_baseline( @@ -283,3 +280,8 @@ async def test_native_export_async_clients_bind_uri_and_query_values( rows = await cursor.fetchall() assert len(rows) == 1 assert rows[0][1] == 1 + + +def _file_uri(destination: str, filename: str) -> str: + parts = urlsplit(destination) + return urlunsplit(parts._replace(path=f"{parts.path}/{filename}")) diff --git a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py index 611ab6737..3e317a38c 100644 --- a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py +++ b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py @@ -4,7 +4,7 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from threading import Thread -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 import duckdb @@ -18,11 +18,17 @@ from sqlspec.adapters.duckdb.driver import DuckDBDriver from sqlspec.storage import StorageFormat, StorageRegistry, SyncStoragePipeline from sqlspec.storage.registry import storage_registry +from sqlspec.utils.module_loader import import_optional_attr from tests.fixtures.rustfs import rustfs_filesystem, rustfs_obstore_kwargs +if TYPE_CHECKING: + from fsspec.implementations.memory import MemoryFileSystem +else: + MemoryFileSystem = import_optional_attr("fsspec.implementations.memory", "MemoryFileSystem") + def test_registered_filesystem_transfers_without_cloud_extensions(monkeypatch: pytest.MonkeyPatch) -> None: - from fsspec.implementations.memory import MemoryFileSystem + pytest.importorskip("fsspec") filesystem = MemoryFileSystem() @@ -136,37 +142,10 @@ def test_best_effort_extension_failure_does_not_enable_native_storage(tmp_path: config.close_pool() -def _local_config(service: RustfsService, *, wrong_key: bool = False) -> DuckDBConfig: - return DuckDBConfig( - connection_config={ - "database": f":memory:duckdb_storage_{uuid4().hex}", - "autoload_known_extensions": False, - "autoinstall_known_extensions": False, - }, - driver_features={ - "extensions": [{"name": "httpfs", "install": True, "required": True}], - "secrets": [ - { - "name": "local_storage", - "secret_type": "s3", - "required": True, - "value": { - "key_id": "invalid-local-key" if wrong_key else service.access_key, - "secret": service.secret_key, - "region": "us-east-1", - "endpoint": service.endpoint, - "use_ssl": service.secure, - "url_style": "path", - }, - } - ], - }, - ) - - def test_local_object_success_missing_and_auth_errors( rustfs_service: RustfsService, rustfs_bucket_name: str, tmp_path: Path ) -> None: + pytest.importorskip("obstore") bucket = rustfs_bucket_name key = f"duckdb-native-probe/{uuid4().hex}.parquet" path = tmp_path / "source.parquet" @@ -202,14 +181,15 @@ def test_local_object_success_missing_and_auth_errors( def test_arrow_and_native_copy_replace_single_local_object( rustfs_service: RustfsService, rustfs_bucket_name: str, file_format: str ) -> None: + pytest.importorskip("obstore") bucket = rustfs_bucket_name alias = f"duckdb_copy_{uuid4().hex}" key = f"duckdb-copy-probe/{uuid4().hex}.{file_format}" options = rustfs_obstore_kwargs(rustfs_service) storage_registry.register_alias(alias, f"s3://{bucket}", backend="obstore", **options) + store = storage_registry.get(alias) config = _local_config(rustfs_service) uri = f"s3://{bucket}/{key}" - fs = rustfs_filesystem(rustfs_service) try: with config.provide_session() as session: for value in [1, 2]: @@ -234,20 +214,22 @@ def test_arrow_and_native_copy_replace_single_local_object( assert session.connection.execute(read_sql, [uri]).fetchall() == [(value,)] finally: config.close_pool() - fs.rm(f"{bucket}/{key}") + if store.exists_sync(key): + store.delete_sync(key) storage_registry.clear_cache(alias) def test_native_parquet_append_and_overwrite_fallback( rustfs_service: RustfsService, rustfs_bucket_name: str, monkeypatch: pytest.MonkeyPatch ) -> None: + pytest.importorskip("obstore") bucket = rustfs_bucket_name alias = f"duckdb_import_{uuid4().hex}" key = f"duckdb-native-import/{uuid4().hex}.parquet" destination = f"alias://{alias}/{key}" storage_registry.register_alias(alias, f"s3://{bucket}", backend="obstore", **rustfs_obstore_kwargs(rustfs_service)) + store = storage_registry.get(alias) config = _local_config(rustfs_service) - fs = rustfs_filesystem(rustfs_service) def reject_arrow(*args: Any, **kwargs: Any) -> Any: raise AssertionError("native append must not materialize Arrow") @@ -269,8 +251,8 @@ def reject_arrow(*args: Any, **kwargs: Any) -> Any: assert session.connection.execute('SELECT * FROM "target space"').fetchall() == [(1, None)] finally: config.close_pool() - if fs.exists(f"{bucket}/{key}"): - fs.rm(f"{bucket}/{key}") + if store.exists_sync(key): + store.delete_sync(key) storage_registry.clear_cache(alias) @@ -278,12 +260,13 @@ def reject_arrow(*args: Any, **kwargs: Any) -> Any: def test_native_export_does_not_materialize_arrow( rustfs_service: RustfsService, rustfs_bucket_name: str, file_format: str, monkeypatch: pytest.MonkeyPatch ) -> None: + pytest.importorskip("obstore") bucket = rustfs_bucket_name alias = f"duckdb_export_{uuid4().hex}" key = f"duckdb-native-export/{uuid4().hex}-quote'.{file_format}" storage_registry.register_alias(alias, f"s3://{bucket}", backend="obstore", **rustfs_obstore_kwargs(rustfs_service)) + store = storage_registry.get(alias) config = _local_config(rustfs_service) - fs = rustfs_filesystem(rustfs_service) def reject_arrow(*args: Any, **kwargs: Any) -> Any: raise AssertionError("native export must not materialize Arrow") @@ -320,6 +303,34 @@ def reject_arrow(*args: Any, **kwargs: Any) -> Any: ] finally: config.close_pool() - if fs.exists(f"{bucket}/{key}"): - fs.rm(f"{bucket}/{key}") + if store.exists_sync(key): + store.delete_sync(key) storage_registry.clear_cache(alias) + + +def _local_config(service: RustfsService, *, wrong_key: bool = False) -> DuckDBConfig: + return DuckDBConfig( + connection_config={ + "database": f":memory:duckdb_storage_{uuid4().hex}", + "autoload_known_extensions": False, + "autoinstall_known_extensions": False, + }, + driver_features={ + "extensions": [{"name": "httpfs", "install": True, "required": True}], + "secrets": [ + { + "name": "local_storage", + "secret_type": "s3", + "required": True, + "value": { + "key_id": "invalid-local-key" if wrong_key else service.access_key, + "secret": service.secret_key, + "region": "us-east-1", + "endpoint": service.endpoint, + "use_ssl": service.secure, + "url_style": "path", + }, + } + ], + }, + ) diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py index 5cb77fabf..c03542f2c 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_maintenance.py @@ -31,27 +31,6 @@ pytestmark = pytest.mark.xdist_group("sqlite") -def _build_memory_record( - *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" -) -> "StoredMemory": - now = datetime.now(UTC) - return StoredMemory( - id=str(uuid4()), - session_id=session_id, - app_name="agent_app", - user_id="user_1", - scope=scope, - event_id=event_id, - author="user", - timestamp=now, - content_json={"text": content_text}, - content_text=content_text, - metadata_json=None, - inserted_at=inserted_at, - embedding=None, - ) - - def test_sqlite_prune_sessions_and_scoped_memory() -> None: """Prune stale sessions and user-scoped memories while preserving app-scoped ones.""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -164,3 +143,24 @@ def test_sqlite_prune_events_scopes_to_app_name() -> None: finally: config.close_pool() Path(db_path).unlink(missing_ok=True) + + +def _build_memory_record( + *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" +) -> "StoredMemory": + now = datetime.now(UTC) + return StoredMemory( + id=str(uuid4()), + session_id=session_id, + app_name="agent_app", + user_id="user_1", + scope=scope, + event_id=event_id, + author="user", + timestamp=now, + content_json={"text": content_text}, + content_text=content_text, + metadata_json=None, + inserted_at=inserted_at, + embedding=None, + ) diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py index bb5203599..95cfbe683 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_memory_store.py @@ -22,27 +22,6 @@ pytestmark = pytest.mark.xdist_group("sqlite") -def _build_record( - *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" -) -> "StoredMemory": - now = datetime.now(UTC) - return StoredMemory( - id=str(uuid4()), - session_id=session_id, - app_name="app", - user_id="user", - scope=scope, - event_id=event_id, - author="user", - timestamp=now, - content_json={"text": content_text}, - content_text=content_text, - metadata_json=None, - inserted_at=inserted_at, - embedding=None, - ) - - def test_sqlite_memory_store_insert_search_dedup() -> None: """Insert memory entries, search by text, and skip duplicates.""" with tempfile.NamedTemporaryFile(suffix=".db") as tmp: @@ -238,3 +217,24 @@ def test_sqlite_memory_store_scoped_retention() -> None: remaining = store.search_entries(query="old", app_name="app", user_id="user") assert len(remaining) == 1 assert remaining[0]["event_id"] == "evt-ao" + + +def _build_record( + *, session_id: str, event_id: str, content_text: str, inserted_at: datetime, scope: str = "user" +) -> "StoredMemory": + now = datetime.now(UTC) + return StoredMemory( + id=str(uuid4()), + session_id=session_id, + app_name="app", + user_id="user", + scope=scope, + event_id=event_id, + author="user", + timestamp=now, + content_json={"text": content_text}, + content_text=content_text, + metadata_json=None, + inserted_at=inserted_at, + embedding=None, + ) diff --git a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py index 217e16bd5..4d67f4c02 100644 --- a/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py +++ b/tests/integration/adapters/sqlite/sqlite/extensions/adk/test_owner_id_column.py @@ -23,55 +23,6 @@ pytestmark = [pytest.mark.xdist_group("sqlite"), pytest.mark.sqlite, pytest.mark.integration] -def _make_shared_memory_db_name() -> str: - """Generate unique shared memory database URI for each test.""" - return f"file:memory_{uuid.uuid4().hex}?mode=memory&cache=shared" - - -def _create_tenants_table(config: SqliteConfig) -> None: - """Create a tenants reference table for FK testing.""" - with config.provide_connection() as conn: - conn.execute("PRAGMA foreign_keys = ON") - conn.execute(""" - CREATE TABLE IF NOT EXISTS tenants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE - ) - """) - conn.commit() - - -def _insert_tenant(config: SqliteConfig, tenant_name: str) -> int | None: - """Insert a tenant and return its ID.""" - with config.provide_connection() as conn: - conn.execute("PRAGMA foreign_keys = ON") - cursor = conn.execute("INSERT INTO tenants (name) VALUES (?)", (tenant_name,)) - tenant_id = cursor.lastrowid - conn.commit() - return tenant_id - - -def _create_users_table(config: SqliteConfig) -> None: - """Create a users reference table for FK testing with TEXT primary key.""" - with config.provide_connection() as conn: - conn.execute("PRAGMA foreign_keys = ON") - conn.execute(""" - CREATE TABLE IF NOT EXISTS users ( - username TEXT PRIMARY KEY, - email TEXT NOT NULL UNIQUE - ) - """) - conn.commit() - - -def _insert_user(config: SqliteConfig, username: str, email: str) -> None: - """Insert a user.""" - with config.provide_connection() as conn: - conn.execute("PRAGMA foreign_keys = ON") - conn.execute("INSERT INTO users (username, email) VALUES (?, ?)", (username, email)) - conn.commit() - - @pytest.fixture def sqlite_config() -> SqliteConfig: """Provide unique shared in-memory SQLite config for each test. @@ -391,3 +342,52 @@ def test_owner_id_with_default_value( assert session["id"] == session_id retrieved = store.get_session(app_name, user_id, session_id) assert retrieved is not None + + +def _make_shared_memory_db_name() -> str: + """Generate unique shared memory database URI for each test.""" + return f"file:memory_{uuid.uuid4().hex}?mode=memory&cache=shared" + + +def _create_tenants_table(config: SqliteConfig) -> None: + """Create a tenants reference table for FK testing.""" + with config.provide_connection() as conn: + conn.execute("PRAGMA foreign_keys = ON") + conn.execute(""" + CREATE TABLE IF NOT EXISTS tenants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE + ) + """) + conn.commit() + + +def _insert_tenant(config: SqliteConfig, tenant_name: str) -> int | None: + """Insert a tenant and return its ID.""" + with config.provide_connection() as conn: + conn.execute("PRAGMA foreign_keys = ON") + cursor = conn.execute("INSERT INTO tenants (name) VALUES (?)", (tenant_name,)) + tenant_id = cursor.lastrowid + conn.commit() + return tenant_id + + +def _create_users_table(config: SqliteConfig) -> None: + """Create a users reference table for FK testing with TEXT primary key.""" + with config.provide_connection() as conn: + conn.execute("PRAGMA foreign_keys = ON") + conn.execute(""" + CREATE TABLE IF NOT EXISTS users ( + username TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE + ) + """) + conn.commit() + + +def _insert_user(config: SqliteConfig, username: str, email: str) -> None: + """Insert a user.""" + with config.provide_connection() as conn: + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("INSERT INTO users (username, email) VALUES (?, ?)", (username, email)) + conn.commit() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 3184f1d7f..c1ed03218 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -7,6 +7,7 @@ import pytest import tests.integration.fixtures as fixtures_pkg +from tests.fixtures.rustfs import ensure_rustfs_bucket if TYPE_CHECKING: from pytest_databases.docker.rustfs import RustfsService @@ -65,8 +66,7 @@ def pytest_configure(config: pytest.Config) -> None: @pytest.fixture(scope="session") def rustfs_bucket_name(rustfs_service: "RustfsService", rustfs_default_bucket_name: str) -> str: """Return the verified RustFS bucket used by S3-compatible integration tests.""" - _ = rustfs_service - return rustfs_default_bucket_name + return ensure_rustfs_bucket(rustfs_service, rustfs_default_bucket_name) def _package_available(package: str) -> bool: diff --git a/tests/integration/fixtures/bigquery.py b/tests/integration/fixtures/bigquery.py index b982ce522..d785e1ad5 100644 --- a/tests/integration/fixtures/bigquery.py +++ b/tests/integration/fixtures/bigquery.py @@ -13,24 +13,6 @@ __all__ = ("bigquery_config", "bigquery_session", "native_bigquery_service", "table_schema_prefix") -def _is_bigquery_emulator(service: "BigQueryService") -> bool: - return getattr(service, "container", None) is not None - - -def _anonymous_credentials() -> "Credentials": - factory = cast("Any", AnonymousCredentials) - return cast("Credentials", factory()) - - -def _bigquery_connection_config(service: "BigQueryService", dataset_id: str | None = None) -> "dict[str, Any]": - return { - "project": service.project, - "dataset_id": dataset_id or f"`{service.project}`.`{service.dataset}`", - "client_options": ClientOptions(api_endpoint=service.endpoint), - "credentials": _anonymous_credentials(), - } - - @pytest.fixture(scope="session") def native_bigquery_service(bigquery_service: "BigQueryService") -> "BigQueryService": """Require a native BigQuery service instead of the Docker emulator.""" @@ -64,3 +46,21 @@ def bigquery_session(bigquery_config: "BigQueryConfig") -> "Generator[BigQueryDr """Create a session-scoped BigQuery driver.""" with bigquery_config.provide_session() as session: yield session + + +def _is_bigquery_emulator(service: "BigQueryService") -> bool: + return getattr(service, "container", None) is not None + + +def _anonymous_credentials() -> "Credentials": + factory = cast("Any", AnonymousCredentials) + return cast("Credentials", factory()) + + +def _bigquery_connection_config(service: "BigQueryService", dataset_id: str | None = None) -> "dict[str, Any]": + return { + "project": service.project, + "dataset_id": dataset_id or f"`{service.project}`.`{service.dataset}`", + "client_options": ClientOptions(api_endpoint=service.endpoint), + "credentials": _anonymous_credentials(), + } From 7116ca4b0bb4e41e8dd3f0ed946856a946a60fd4 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 17:22:32 +0000 Subject: [PATCH 06/35] test(xdist): preserve distinct container xdist groups and fix adbc family routing --- .../_shared/suite_case_metadata_contract.py | 82 +++++++++++++++++++ .../test_retry.py} | 30 +++---- tests/integration/adapters/conftest.py | 30 ++++++- 3 files changed, 125 insertions(+), 17 deletions(-) rename tests/integration/adapters/{postgres/test_cockroach_retry.py => cockroach/test_retry.py} (85%) diff --git a/tests/integration/adapters/_shared/suite_case_metadata_contract.py b/tests/integration/adapters/_shared/suite_case_metadata_contract.py index 83ffa6c6f..110a466cf 100644 --- a/tests/integration/adapters/_shared/suite_case_metadata_contract.py +++ b/tests/integration/adapters/_shared/suite_case_metadata_contract.py @@ -3,6 +3,8 @@ from pathlib import Path from typing import cast +import msgspec +import pytest from _pytest.mark.structures import ParameterSet from tests.integration.adapters._shared._adk_cases import AdkStoreCase, adk_store_params_with @@ -26,6 +28,7 @@ ASYNC_POSTGRES_EXTENSION_CASES, SYNC_POSTGRES_EXTENSION_CASES, ) +from tests.integration.adapters.conftest import _case_family, pytest_collection_modifyitems MYSQL_ROW_LOCKING_CASE_IDS = { "aiomysql-async", @@ -127,6 +130,7 @@ "oracledb/test_merge_bulk.py", "oracledb/test_stack.py", "oracledb/test_vector_functions.py", + "postgres/test_cockroach_retry.py", "psqlpy/test_data_dictionary.py", "psqlpy/test_merge_bulk.py", "psqlpy/test_migrations.py", @@ -288,6 +292,84 @@ def test_parameter_style_params_are_method_filtered() -> None: } +def test_case_family_routes_adbc_dialects() -> None: + """ADBC case identifiers route to their backing database family instead of defaulting to postgres.""" + expected_routes = { + "adbc-oracle": "oracle", + "adbc-db2": "db2", + "adbc-snowflake": "snowflake", + "adbc-bigquery": "bigquery", + "adbc-duckdb": "duckdb", + "adbc-sqlite": "sqlite", + "adbc-postgres": "postgres", + "adbc-pgvector": "postgres", + "adbc-gizmosql-duckdb": "gizmosql", + } + observed_routes = {case_id: _case_family(_StubCaseId(id=case_id)) for case_id in expected_routes} + assert observed_routes == expected_routes + + +def test_collection_preserves_container_distinct_xdist_groups( + request: pytest.FixtureRequest, pytestconfig: pytest.Config +) -> None: + """Container-distinct xdist_group markers survive family xdist normalization.""" + adapters_root = Path(__file__).resolve().parents[1] + preserved_groups = ( + ("postgres", "alloydb"), + ("gizmosql", "gizmosql_sqlite"), + ("mysql", "mariadb"), + ("oracle", "oracle_18c"), + ("oracle", "oracle_adb"), + ("postgres", "paradedb"), + ("postgres", "pgvector"), + ("snowflake", "snowflake"), + ) + items: list[pytest.Item] = [] + for family, group_name in preserved_groups: + item = _StubAdapterItem.from_parent( + request.session, name=f"test_{group_name}", path=adapters_root / family / f"test_{group_name}.py" + ) + item.add_marker(pytest.mark.xdist_group(group_name)) + items.append(item) + + ungrouped_postgres = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_postgres", path=adapters_root / "postgres" / "test_driver.py" + ) + overridden_postgres = _StubAdapterItem.from_parent( + request.session, name="test_overridden_postgres", path=adapters_root / "postgres" / "test_driver.py" + ) + overridden_postgres.add_marker(pytest.mark.xdist_group("custom_transient_group")) + ungrouped_cockroach = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_cockroach", path=adapters_root / "cockroach" / "test_retry.py" + ) + ungrouped_snowflake = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_snowflake", path=adapters_root / "snowflake" / "test_adbc.py" + ) + items.extend((ungrouped_postgres, overridden_postgres, ungrouped_cockroach, ungrouped_snowflake)) + + pytest_collection_modifyitems(pytestconfig, items) + + for item, (_, expected_group) in zip(items[: len(preserved_groups)], preserved_groups, strict=True): + assert _xdist_groups(item) == [expected_group] + assert _xdist_groups(ungrouped_postgres) == ["postgres"] + assert _xdist_groups(overridden_postgres) == ["postgres"] + assert _xdist_groups(ungrouped_cockroach) == ["cockroachdb"] + assert _xdist_groups(ungrouped_snowflake) == ["snowflake"] + + +class _StubCaseId(msgspec.Struct, frozen=True, gc=False): + id: str + + +class _StubAdapterItem(pytest.Item): + def runtest(self) -> None: + return None + + +def _xdist_groups(item: pytest.Item) -> list[str]: + return [str(mark.args[0]) for mark in item.iter_markers(name="xdist_group") if mark.args] + + def _driver_cases(params: tuple[ParameterSet, ...]) -> tuple[DriverCase, ...]: return tuple(cast("DriverCase", param.values[0]) for param in params) diff --git a/tests/integration/adapters/postgres/test_cockroach_retry.py b/tests/integration/adapters/cockroach/test_retry.py similarity index 85% rename from tests/integration/adapters/postgres/test_cockroach_retry.py rename to tests/integration/adapters/cockroach/test_retry.py index acf18323e..d18daac0e 100644 --- a/tests/integration/adapters/postgres/test_cockroach_retry.py +++ b/tests/integration/adapters/cockroach/test_retry.py @@ -7,12 +7,13 @@ from sqlspec import StatementStack from sqlspec.exceptions import SerializationConflictError +if TYPE_CHECKING: + from collections.abc import Callable -class _SqlstateConflict(Exception): - """A raw driver error that carries the conflict only as a SQLSTATE.""" - - sqlstate = "40001" + from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgDriver + from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncDriver, CockroachPsycopgSyncDriver +pytestmark = [pytest.mark.integration, pytest.mark.xdist_group("cockroachdb")] _CONFLICTS = pytest.mark.parametrize( "conflict", @@ -22,19 +23,12 @@ class _SqlstateConflict(Exception): ], ) -if TYPE_CHECKING: - from collections.abc import Callable - - from sqlspec.adapters.cockroach_asyncpg import CockroachAsyncpgDriver - from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgAsyncDriver, CockroachPsycopgSyncDriver - -pytestmark = pytest.mark.xdist_group("postgres") - @_CONFLICTS def test_cockroach_psycopg_sync_retries_whole_transaction( contract_cockroach_psycopg_sync_driver: "CockroachPsycopgSyncDriver", conflict: "Callable[[], Exception]" ) -> None: + """Verify sync psycopg CockroachDB retries restart and commit the whole transaction.""" calls = 0 def operation() -> str: @@ -44,7 +38,7 @@ def operation() -> str: StatementStack().push_execute("SELECT 1 AS transaction_probe") ) assert len(results) == 1 - assert contract_cockroach_psycopg_sync_driver._transaction_active is True # pyright: ignore[reportPrivateUsage] + assert contract_cockroach_psycopg_sync_driver._transaction_active is True assert contract_cockroach_psycopg_sync_driver.connection.autocommit is False if calls == 1: raise conflict() @@ -58,6 +52,7 @@ def operation() -> str: async def test_cockroach_asyncpg_retries_whole_transaction( contract_cockroach_asyncpg_driver: "CockroachAsyncpgDriver", conflict: "Callable[[], Exception]" ) -> None: + """Verify asyncpg CockroachDB retries restart and commit the whole transaction.""" calls = 0 async def operation() -> str: @@ -75,6 +70,7 @@ async def operation() -> str: async def test_cockroach_psycopg_async_retries_whole_transaction( contract_cockroach_psycopg_async_driver: "CockroachPsycopgAsyncDriver", conflict: "Callable[[], Exception]" ) -> None: + """Verify async psycopg CockroachDB retries restart and commit the whole transaction.""" calls = 0 async def operation() -> str: @@ -84,7 +80,7 @@ async def operation() -> str: StatementStack().push_execute("SELECT 1 AS transaction_probe") ) assert len(results) == 1 - assert contract_cockroach_psycopg_async_driver._transaction_active is True # pyright: ignore[reportPrivateUsage] + assert contract_cockroach_psycopg_async_driver._transaction_active is True assert contract_cockroach_psycopg_async_driver.connection.autocommit is False if calls == 1: raise conflict() @@ -92,3 +88,9 @@ async def operation() -> str: assert await contract_cockroach_psycopg_async_driver.run_transaction_with_retry(operation) == "ok" assert calls == 2 + + +class _SqlstateConflict(Exception): + """Raw driver error carrying the serialization conflict only as SQLSTATE 40001.""" + + sqlstate = "40001" diff --git a/tests/integration/adapters/conftest.py b/tests/integration/adapters/conftest.py index 9627a7941..e8eeaae0b 100644 --- a/tests/integration/adapters/conftest.py +++ b/tests/integration/adapters/conftest.py @@ -14,17 +14,28 @@ "spanner": frozenset({"spanner"}), "sqlite": frozenset({"aiosqlite", "sqlite"}), } -_SHARED_TEST_FAMILIES = frozenset((*_FAMILY_ADAPTERS, "gizmosql")) +_SHARED_TEST_FAMILIES = frozenset((*_FAMILY_ADAPTERS, "gizmosql", "snowflake")) _FAMILY_XDIST_GROUPS = {family: "cockroachdb" if family == "cockroach" else family for family in _SHARED_TEST_FAMILIES} +_PRESERVED_CONTAINER_XDIST_GROUPS: frozenset[str] = frozenset({ + "alloydb", + "gizmosql_sqlite", + "mariadb", + "oracle_18c", + "oracle_adb", + "paradedb", + "pgvector", + "snowflake", +}) +@pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: - """Keep shared cases in their family and serialize every family on one worker.""" + """Keep shared cases in their family while preserving distinct container xdist groups.""" retained: list[pytest.Item] = [] deselected: list[pytest.Item] = [] for item in items: item_family = next((part for part in item.path.parts if part in _SHARED_TEST_FAMILIES), None) - if item_family is not None: + if item_family is not None and not _has_preserved_container_xdist_group(item): item.own_markers[:] = [mark for mark in item.own_markers if mark.name != "xdist_group"] item.add_marker(pytest.mark.xdist_group(_FAMILY_XDIST_GROUPS[item_family])) if item.path.name != "test_shared.py" or item.path.parent.name not in _SHARED_TEST_FAMILIES: @@ -43,6 +54,13 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item items[:] = retained +def _has_preserved_container_xdist_group(item: pytest.Item) -> bool: + return any( + mark.args and str(mark.args[0]) in _PRESERVED_CONTAINER_XDIST_GROUPS + for mark in item.iter_markers(name="xdist_group") + ) + + def _case_family(value: object) -> str | None: case = getattr(value, "case", value) adapter = getattr(case, "adapter", None) @@ -67,6 +85,12 @@ def _case_family(value: object) -> str | None: return "duckdb" if "bigquery" in identity: return "bigquery" + if "oracle" in identity: + return "oracle" + if "db2" in identity: + return "db2" + if "snowflake" in identity: + return "snowflake" return "postgres" for family, adapters in _FAMILY_ADAPTERS.items(): if adapter in adapters: From afca502e18052b53098df84134df190d8a81dd58 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 17:27:22 +0000 Subject: [PATCH 07/35] ci(integration): shard integration matrix across 5 parallel service groups --- .github/workflows/ci.yml | 83 +++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5343c2005..f4793effc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,6 +260,7 @@ jobs: -n auto --dist=loadgroup -p no:cacheprovider test-integration: + name: "Integration (Py ${{ matrix.python-version }}, ${{ matrix.shard }})" needs: [ci-scope, test-unit] if: needs.ci-scope.outputs.ci_required == 'true' runs-on: ubuntu-24.04 @@ -268,7 +269,39 @@ jobs: fail-fast: false matrix: python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] - timeout-minutes: 60 + shard: + - postgres-cockroach + - mysql-mariadb-embedded + - oracle + - mssql-db2 + - cloud-adbc-storage + include: + - shard: postgres-cockroach + install_odbc: false + images: "postgres:18 pgvector/pgvector:pg18 paradedb/paradedb:latest-pg18 cockroachdb/cockroach:latest rustfs/rustfs:latest rustfs/rc:latest" + pytest_targets: "tests/integration/adapters/postgres tests/integration/adapters/cockroach tests/integration/extensions tests/integration/observability" + extra_pytest_flags: "" + - shard: mysql-mariadb-embedded + install_odbc: false + images: "mysql:8.4 rustfs/rustfs:latest rustfs/rc:latest" + pytest_targets: "tests/integration/adapters/mysql tests/integration/adapters/sqlite tests/integration/adapters/duckdb tests/integration/cli tests/integration/migrations" + extra_pytest_flags: "" + - shard: oracle + install_odbc: false + images: "gvenzl/oracle-xe:18-slim-faststart gvenzl/oracle-free:23-slim-faststart" + pytest_targets: "tests/integration/adapters/oracle" + extra_pytest_flags: "" + - shard: mssql-db2 + install_odbc: true + images: "mcr.microsoft.com/mssql/server:2022-latest icr.io/db2_community/db2:11.5.9.0" + pytest_targets: "tests/integration/adapters/mssql tests/integration/adapters/db2" + extra_pytest_flags: "" + - shard: cloud-adbc-storage + install_odbc: false + images: "ghcr.io/goccy/bigquery-emulator:latest gcr.io/cloud-spanner-emulator/emulator:latest gizmodata/gizmosql:latest rustfs/rustfs:latest rustfs/rc:latest" + pytest_targets: "tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage" + extra_pytest_flags: "--run-bigquery-tests --run-spanner-tests" + timeout-minutes: 30 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -327,7 +360,7 @@ jobs: df -h / - name: Install SQL Server ODBC driver - if: steps.install-deps.outcome == 'success' + if: steps.install-deps.outcome == 'success' && matrix.install_odbc run: | set -eu key_url="https://packages.microsoft.com/keys/microsoft.asc" @@ -366,7 +399,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: /tmp/docker-images - key: docker-images-v2-${{ hashFiles('.github/workflows/ci.yml') }} + key: docker-images-${{ matrix.shard }}-v2-${{ hashFiles('.github/workflows/ci.yml') }} - name: Load cached docker images if: steps.install-deps.outcome == 'success' && steps.docker-image-cache.outputs.cache-hit == 'true' @@ -376,24 +409,11 @@ jobs: - name: Pre-pull docker images if: steps.install-deps.outcome == 'success' && steps.docker-image-cache.outputs.cache-hit != 'true' + env: + SHARD_IMAGES: ${{ matrix.images }} run: | set -eu - images=( - postgres:18 - gizmodata/gizmosql:latest - pgvector/pgvector:pg15 - paradedb/paradedb:latest-pg18 - cockroachdb/cockroach:latest - mysql:8.4 - mcr.microsoft.com/mssql/server:2022-latest - gvenzl/oracle-xe:18-slim-faststart - gvenzl/oracle-free:23-slim-faststart - icr.io/db2_community/db2:11.5.9.0 - ghcr.io/goccy/bigquery-emulator:latest - gcr.io/cloud-spanner-emulator/emulator:latest - rustfs/rustfs:latest - rustfs/rc:latest - ) + read -r -a images <<< "$SHARD_IMAGES" printf '%s\n' "${images[@]}" | xargs -P 4 -I{} bash -c ' for attempt in 1 2 3; do if docker pull "$1"; then @@ -415,7 +435,10 @@ jobs: env: PYTHONFAULTHANDLER: "1" PYTEST_ADDOPTS: "--max-worker-restart=0" - # At 35min, signal the pytest process group with SIGABRT so faulthandler + SHARD_NAME: ${{ matrix.shard }} + PYTEST_TARGETS: ${{ matrix.pytest_targets }} + EXTRA_PYTEST_FLAGS: ${{ matrix.extra_pytest_flags }} + # At 22min, signal the pytest process group with SIGABRT so faulthandler # dumps thread stacks to stderr, with bounded escalation to SIGTERM and SIGKILL. # Background watchdog is trapped and reaped reliably on any exit. run: | @@ -441,20 +464,26 @@ jobs: trap 'exit 130' INT trap 'exit 143' TERM + read -r -a targets <<< "$PYTEST_TARGETS" + extra_flags=() + if [ -n "$EXTRA_PYTEST_FLAGS" ]; then + read -r -a extra_flags <<< "$EXTRA_PYTEST_FLAGS" + fi + # covdefaults sets fail_under=100 globally; per-tier coverage # only covers a slice, so override to 0. Codecov merges the # tiers server-side. - uv run pytest tests/integration -n 2 --dist=loadgroup \ - --cov=sqlspec --cov-report=xml:coverage-integration.xml --cov-report=term-missing \ + uv run pytest "${targets[@]}" "${extra_flags[@]}" -n 2 --dist=loadgroup \ + --cov=sqlspec --cov-report="xml:coverage-integration-${SHARD_NAME}.xml" --cov-report=term-missing \ --cov-fail-under=0 & pytest_pid=$! # Job control makes the background job leader its process-group leader. pytest_pgid=$pytest_pid ( - sleep 2100 + sleep 1320 if kill -0 -"$pytest_pgid" 2>/dev/null; then - echo "::group::Hang detected after 35 minutes - SIGABRT to pytest process group $pytest_pgid" + echo "::group::Hang detected after 22 minutes - SIGABRT to pytest process group $pytest_pgid" kill -ABRT -"$pytest_pgid" 2>/dev/null || true sleep 15 if kill -0 -"$pytest_pgid" 2>/dev/null; then @@ -475,9 +504,9 @@ jobs: if: always() && steps.pytest.outcome == 'success' uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: - files: coverage-integration.xml - flags: integration,py${{ matrix.python-version }} - name: test-integration-${{ matrix.python-version }} + files: coverage-integration-${{ matrix.shard }}.xml + flags: integration,py${{ matrix.python-version }},${{ matrix.shard }} + name: test-integration-${{ matrix.python-version }}-${{ matrix.shard }} fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} From 6fbd4272f0f06369595d1ce3f51cd26c97d070f9 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 17:30:29 +0000 Subject: [PATCH 08/35] build(makefile): parallelize integration tests and add per-shard targets --- Makefile | 67 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index f49814267..7c45b1e99 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,13 @@ SHELL := /bin/bash .EXPORT_ALL_VARIABLES: MAKEFLAGS += --no-print-directory MYPY_WORKERS ?= 2 -ADAPTER_FAMILIES := bigquery cockroach db2 duckdb gizmosql mssql mysql oracle postgres spanner sqlite +UNIT_WORKERS ?= 4 +INTEGRATION_WORKERS ?= 2 +SHARD_POSTGRES_COCKROACH := tests/integration/adapters/postgres tests/integration/adapters/cockroach tests/integration/extensions tests/integration/observability +SHARD_MYSQL_MARIADB_EMBEDDED := tests/integration/adapters/mysql tests/integration/adapters/sqlite tests/integration/adapters/duckdb tests/integration/cli tests/integration/migrations +SHARD_ORACLE := tests/integration/adapters/oracle +SHARD_MSSQL_DB2 := tests/integration/adapters/mssql tests/integration/adapters/db2 +SHARD_CLOUD_ADBC_STORAGE := tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage # ----------------------------------------------------------------------------- # Display Formatting and Colors @@ -169,13 +175,52 @@ clean: ## Cleanup temporary build a .PHONY: test test: ## Run the tests @echo "${INFO} Running test cases... 🧪" - @uv run pytest -n 4 --dist=loadgroup tests/unit - @uv run pytest -n 1 --dist=loadgroup tests/integration --ignore=tests/integration/adapters - @for family in $(ADAPTER_FAMILIES); do - uv run pytest -n 1 --dist=loadgroup "tests/integration/adapters/$${family}" - done + @uv run pytest -n $(UNIT_WORKERS) --dist=loadgroup tests/unit + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup tests/integration @echo "${OK} Tests complete ✨" +.PHONY: test-unit +test-unit: ## Run unit tests + @echo "${INFO} Running unit tests... 🧪" + @uv run pytest -n $(UNIT_WORKERS) --dist=loadgroup tests/unit + @echo "${OK} Unit tests complete ✨" + +.PHONY: test-integration +test-integration: ## Run integration tests + @echo "${INFO} Running integration tests... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup tests/integration + @echo "${OK} Integration tests complete ✨" + +.PHONY: test-integration-postgres-cockroach +test-integration-postgres-cockroach: ## Run Postgres and CockroachDB integration shard + @echo "${INFO} Running postgres-cockroach integration shard... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup $(SHARD_POSTGRES_COCKROACH) + @echo "${OK} postgres-cockroach integration shard complete ✨" + +.PHONY: test-integration-mysql-mariadb-embedded +test-integration-mysql-mariadb-embedded: ## Run MySQL, MariaDB, and embedded integration shard + @echo "${INFO} Running mysql-mariadb-embedded integration shard... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup $(SHARD_MYSQL_MARIADB_EMBEDDED) + @echo "${OK} mysql-mariadb-embedded integration shard complete ✨" + +.PHONY: test-integration-oracle +test-integration-oracle: ## Run Oracle integration shard + @echo "${INFO} Running oracle integration shard... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup $(SHARD_ORACLE) + @echo "${OK} oracle integration shard complete ✨" + +.PHONY: test-integration-mssql-db2 +test-integration-mssql-db2: ## Run SQL Server and Db2 integration shard + @echo "${INFO} Running mssql-db2 integration shard... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup $(SHARD_MSSQL_DB2) + @echo "${OK} mssql-db2 integration shard complete ✨" + +.PHONY: test-integration-cloud-adbc-storage +test-integration-cloud-adbc-storage: ## Run BigQuery, Spanner, GizmoSQL, and storage shard + @echo "${INFO} Running cloud-adbc-storage integration shard... 🧪" + @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup --run-bigquery-tests --run-spanner-tests $(SHARD_CLOUD_ADBC_STORAGE) + @echo "${OK} cloud-adbc-storage integration shard complete ✨" + .PHONY: test-all test-all: test ## Run all tests @echo "${INFO} All tests executed successfully ✨" @@ -184,14 +229,10 @@ test-all: test ## Run all tests coverage: ## Run tests with coverage report @echo "${INFO} Running tests with coverage... 📊" @uv run coverage erase - @uv run pytest --cov --cov-report= --cov-fail-under=0 -n 4 --dist=loadgroup --quiet \ + @uv run pytest --cov --cov-report= --cov-fail-under=0 -n $(UNIT_WORKERS) --dist=loadgroup --quiet \ tests/unit - @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n 1 --dist=loadgroup --quiet \ - tests/integration --ignore=tests/integration/adapters - @for family in $(ADAPTER_FAMILIES); do - uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n 1 --dist=loadgroup --quiet \ - "tests/integration/adapters/$${family}" - done + @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --quiet \ + tests/integration @uv run coverage report --fail-under=0 @uv run coverage html >/dev/null 2>&1 @uv run coverage xml >/dev/null 2>&1 From dd8a7e64129cda60659c36c1306ca9272a9b58e8 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 17:52:17 +0000 Subject: [PATCH 09/35] test(xdist): strip ancestor xdist_group markers and isolate secondary containers --- .../_shared/suite_case_metadata_contract.py | 106 +++++++++++++++--- tests/integration/adapters/conftest.py | 44 ++++++-- .../adapters/gizmosql/adbc/test_driver.py | 17 +-- .../oracledb/test_json_storage_modes.py | 17 +-- 4 files changed, 143 insertions(+), 41 deletions(-) diff --git a/tests/integration/adapters/_shared/suite_case_metadata_contract.py b/tests/integration/adapters/_shared/suite_case_metadata_contract.py index 110a466cf..6d3d1b604 100644 --- a/tests/integration/adapters/_shared/suite_case_metadata_contract.py +++ b/tests/integration/adapters/_shared/suite_case_metadata_contract.py @@ -312,25 +312,71 @@ def test_case_family_routes_adbc_dialects() -> None: def test_collection_preserves_container_distinct_xdist_groups( request: pytest.FixtureRequest, pytestconfig: pytest.Config ) -> None: - """Container-distinct xdist_group markers survive family xdist normalization.""" + """Container-distinct xdist_group markers survive multi-level collector normalization without compound leakage.""" adapters_root = Path(__file__).resolve().parents[1] preserved_groups = ( - ("postgres", "alloydb"), - ("gizmosql", "gizmosql_sqlite"), - ("mysql", "mariadb"), - ("oracle", "oracle_18c"), - ("oracle", "oracle_adb"), - ("postgres", "paradedb"), - ("postgres", "pgvector"), - ("snowflake", "snowflake"), + ("postgres", "alloydb", False), + ("gizmosql", "gizmosql_sqlite", True), + ("mysql", "mariadb", True), + ("oracle", "oracle_18c", False), + ("oracle", "oracle_adb", True), + ("postgres", "paradedb", True), + ("postgres", "pgvector", False), + ("snowflake", "snowflake", False), ) items: list[pytest.Item] = [] - for family, group_name in preserved_groups: - item = _StubAdapterItem.from_parent( - request.session, name=f"test_{group_name}", path=adapters_root / family / f"test_{group_name}.py" + preserved_expectations: list[tuple[pytest.Item, str]] = [] + for family, group_name, use_keyword in preserved_groups: + family_pkg = _StubAdapterCollector.from_parent( + request.session, name=family, path=adapters_root / family / "__init__.py" ) - item.add_marker(pytest.mark.xdist_group(group_name)) - items.append(item) + family_pkg.add_marker(pytest.mark.xdist_group("cockroachdb" if family == "cockroach" else family)) + module_collector = _StubAdapterCollector.from_parent( + family_pkg, name=f"test_{group_name}.py", path=adapters_root / family / f"test_{group_name}.py" + ) + group_marker = pytest.mark.xdist_group(name=group_name) if use_keyword else pytest.mark.xdist_group(group_name) + module_collector.add_marker(group_marker) + first_item = _StubAdapterItem.from_parent( + module_collector, name=f"test_{group_name}_first", path=module_collector.path + ) + second_item = _StubAdapterItem.from_parent( + module_collector, name=f"test_{group_name}_second", path=module_collector.path + ) + items.extend((first_item, second_item)) + preserved_expectations.extend(((first_item, group_name), (second_item, group_name))) + + oracle_pkg = _StubAdapterCollector.from_parent( + request.session, name="oracledb", path=adapters_root / "oracle" / "oracledb" / "__init__.py" + ) + oracle_pkg.add_marker(pytest.mark.xdist_group("oracle")) + oracle_json_mod = _StubAdapterCollector.from_parent( + oracle_pkg, + name="test_json_storage_modes.py", + path=adapters_root / "oracle" / "oracledb" / "test_json_storage_modes.py", + ) + oracle_json_mod.add_marker(pytest.mark.xdist_group("oracle")) + default_oracle_item = _StubAdapterItem.from_parent( + oracle_json_mod, name="test_native_json_round_trip_matrix", path=oracle_json_mod.path + ) + oracle_18c_item = _StubAdapterItem.from_parent( + oracle_json_mod, name="test_blob_is_json_round_trip_on_oracle_18c", path=oracle_json_mod.path + ) + oracle_18c_item.add_marker(pytest.mark.xdist_group("oracle_18c")) + oracle_adb_cls = _StubAdapterCollector.from_parent( + oracle_json_mod, name="TestOracleAdbWallet", path=oracle_json_mod.path + ) + oracle_adb_cls.add_marker(pytest.mark.xdist_group(name="oracle_adb")) + oracle_adb_class_item = _StubAdapterItem.from_parent( + oracle_adb_cls, name="test_wallet_mtls", path=oracle_json_mod.path + ) + + cloud_mod = _StubAdapterCollector.from_parent( + request.session, + name="test_cloud_connectors.py", + path=adapters_root / "postgres" / "asyncpg" / "test_cloud_connectors.py", + ) + cloud_mod.add_marker(pytest.mark.xdist_group("google_cloud")) + cloud_postgres_item = _StubAdapterItem.from_parent(cloud_mod, name="test_cloud_sql_connector", path=cloud_mod.path) ungrouped_postgres = _StubAdapterItem.from_parent( request.session, name="test_ungrouped_postgres", path=adapters_root / "postgres" / "test_driver.py" @@ -338,19 +384,32 @@ def test_collection_preserves_container_distinct_xdist_groups( overridden_postgres = _StubAdapterItem.from_parent( request.session, name="test_overridden_postgres", path=adapters_root / "postgres" / "test_driver.py" ) - overridden_postgres.add_marker(pytest.mark.xdist_group("custom_transient_group")) + overridden_postgres.add_marker(pytest.mark.xdist_group(name="custom_transient_group")) ungrouped_cockroach = _StubAdapterItem.from_parent( request.session, name="test_ungrouped_cockroach", path=adapters_root / "cockroach" / "test_retry.py" ) ungrouped_snowflake = _StubAdapterItem.from_parent( request.session, name="test_ungrouped_snowflake", path=adapters_root / "snowflake" / "test_adbc.py" ) - items.extend((ungrouped_postgres, overridden_postgres, ungrouped_cockroach, ungrouped_snowflake)) + items.extend(( + default_oracle_item, + oracle_18c_item, + oracle_adb_class_item, + cloud_postgres_item, + ungrouped_postgres, + overridden_postgres, + ungrouped_cockroach, + ungrouped_snowflake, + )) pytest_collection_modifyitems(pytestconfig, items) - for item, (_, expected_group) in zip(items[: len(preserved_groups)], preserved_groups, strict=True): + for item, expected_group in preserved_expectations: assert _xdist_groups(item) == [expected_group] + assert _xdist_groups(default_oracle_item) == ["oracle"] + assert _xdist_groups(oracle_18c_item) == ["oracle_18c"] + assert _xdist_groups(oracle_adb_class_item) == ["oracle_adb"] + assert _xdist_groups(cloud_postgres_item) == ["postgres"] assert _xdist_groups(ungrouped_postgres) == ["postgres"] assert _xdist_groups(overridden_postgres) == ["postgres"] assert _xdist_groups(ungrouped_cockroach) == ["cockroachdb"] @@ -361,13 +420,24 @@ class _StubCaseId(msgspec.Struct, frozen=True, gc=False): id: str +class _StubAdapterCollector(pytest.Collector): + def collect(self) -> list[pytest.Item | pytest.Collector]: + return [] + + class _StubAdapterItem(pytest.Item): def runtest(self) -> None: return None def _xdist_groups(item: pytest.Item) -> list[str]: - return [str(mark.args[0]) for mark in item.iter_markers(name="xdist_group") if mark.args] + groups: list[str] = [] + for mark in item.iter_markers(name="xdist_group"): + if mark.args: + groups.append(str(mark.args[0])) + elif (name := mark.kwargs.get("name")) is not None: + groups.append(str(name)) + return groups def _driver_cases(params: tuple[ParameterSet, ...]) -> tuple[DriverCase, ...]: diff --git a/tests/integration/adapters/conftest.py b/tests/integration/adapters/conftest.py index e8eeaae0b..63acf0525 100644 --- a/tests/integration/adapters/conftest.py +++ b/tests/integration/adapters/conftest.py @@ -1,7 +1,12 @@ """Database-family integration collection rules.""" +from typing import TYPE_CHECKING + import pytest +if TYPE_CHECKING: + from _pytest.nodes import Node + _FAMILY_ADAPTERS = { "bigquery": frozenset({"bigquery"}), "cockroach": frozenset({"cockroach_asyncpg", "cockroach_psycopg"}), @@ -30,16 +35,17 @@ @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: - """Keep shared cases in their family while preserving distinct container xdist groups.""" + """Keep shared cases in their family while normalizing each item to a single xdist group.""" retained: list[pytest.Item] = [] deselected: list[pytest.Item] = [] + resolved_groups: list[tuple[pytest.Item, str]] = [] for item in items: item_family = next((part for part in item.path.parts if part in _SHARED_TEST_FAMILIES), None) - if item_family is not None and not _has_preserved_container_xdist_group(item): - item.own_markers[:] = [mark for mark in item.own_markers if mark.name != "xdist_group"] - item.add_marker(pytest.mark.xdist_group(_FAMILY_XDIST_GROUPS[item_family])) + target_group = _resolve_target_xdist_group(item, item_family) if item_family is not None else None if item.path.name != "test_shared.py" or item.path.parent.name not in _SHARED_TEST_FAMILIES: retained.append(item) + if target_group is not None: + resolved_groups.append((item, target_group)) continue family = item.path.parent.name callspec = getattr(item, "callspec", None) @@ -47,18 +53,38 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item case_families = {case_family for value in values if (case_family := _case_family(value)) is not None} if family in case_families or (not case_families and family == "sqlite"): retained.append(item) + if target_group is not None: + resolved_groups.append((item, target_group)) else: deselected.append(item) + + cleaned_nodes: set[Node] = set() + for item, target_group in resolved_groups: + for node in item.listchain(): + if node not in cleaned_nodes: + node.own_markers[:] = [mark for mark in node.own_markers if mark.name != "xdist_group"] + cleaned_nodes.add(node) + item.add_marker(pytest.mark.xdist_group(target_group)) + if deselected: config.hook.pytest_deselected(items=deselected) items[:] = retained -def _has_preserved_container_xdist_group(item: pytest.Item) -> bool: - return any( - mark.args and str(mark.args[0]) in _PRESERVED_CONTAINER_XDIST_GROUPS - for mark in item.iter_markers(name="xdist_group") - ) +def _resolve_target_xdist_group(item: pytest.Item, item_family: str) -> str: + for mark in item.iter_markers(name="xdist_group"): + group_name = _xdist_group_name(mark) + if group_name in _PRESERVED_CONTAINER_XDIST_GROUPS: + return group_name + return _FAMILY_XDIST_GROUPS[item_family] + + +def _xdist_group_name(mark: pytest.Mark) -> str | None: + if mark.args: + return str(mark.args[0]) + if (name := mark.kwargs.get("name")) is not None: + return str(name) + return None def _case_family(value: object) -> str | None: diff --git a/tests/integration/adapters/gizmosql/adbc/test_driver.py b/tests/integration/adapters/gizmosql/adbc/test_driver.py index d365ed45a..db08a1208 100644 --- a/tests/integration/adapters/gizmosql/adbc/test_driver.py +++ b/tests/integration/adapters/gizmosql/adbc/test_driver.py @@ -34,7 +34,9 @@ def gizmosql_session_case(request: pytest.FixtureRequest) -> GizmoSQLSessionCase @pytest.fixture( params=[ pytest.param(("adbc_gizmosql_config", "duckdb"), id="duckdb"), - pytest.param(("adbc_gizmosql_sqlite_config", "sqlite"), id="sqlite"), + pytest.param( + ("adbc_gizmosql_sqlite_config", "sqlite"), id="sqlite", marks=pytest.mark.xdist_group("gizmosql_sqlite") + ), ] ) def gizmosql_config_case(request: pytest.FixtureRequest) -> GizmoSQLConfigCase: @@ -44,12 +46,6 @@ def gizmosql_config_case(request: pytest.FixtureRequest) -> GizmoSQLConfigCase: return request.getfixturevalue(fixture_name), expected_dialect -def _count_rows(session: AdbcDriver, name: str) -> int: - result = session.execute("SELECT COUNT(*) AS count FROM test_table_adbc WHERE name = ?", (name,)) - assert result.data is not None - return int(result.get_data()[0]["count"]) - - @xfail_if_driver_missing def test_gizmosql_provides_connection_and_session(gizmosql_config_case: GizmoSQLConfigCase) -> None: """GizmoSQL configs should provide raw ADBC connections and sqlspec sessions.""" @@ -65,6 +61,7 @@ def test_gizmosql_provides_connection_and_session(gizmosql_config_case: GizmoSQL @xfail_if_driver_missing +@pytest.mark.xdist_group("gizmosql_sqlite") @pytest.mark.xfail(reason=SQLITE_FLIGHTSQL_RESULT_XFAIL, strict=False) def test_gizmosql_sqlite_result_streams_fetch(adbc_gizmosql_sqlite_config: AdbcConfig) -> None: """GizmoSQL SQLite should return stable FlightSQL result schemas.""" @@ -211,3 +208,9 @@ def test_gizmosql_explain_select(gizmosql_session_case: GizmoSQLSessionCase) -> assert isinstance(result, SQLResult) assert result.data is not None assert len(result.data) >= 1 + + +def _count_rows(session: AdbcDriver, name: str) -> int: + result = session.execute("SELECT COUNT(*) AS count FROM test_table_adbc WHERE name = ?", (name,)) + assert result.data is not None + return int(result.get_data()[0]["count"]) diff --git a/tests/integration/adapters/oracle/oracledb/test_json_storage_modes.py b/tests/integration/adapters/oracle/oracledb/test_json_storage_modes.py index 602640593..73bca63a5 100644 --- a/tests/integration/adapters/oracle/oracledb/test_json_storage_modes.py +++ b/tests/integration/adapters/oracle/oracledb/test_json_storage_modes.py @@ -10,13 +10,7 @@ pytestmark = pytest.mark.xdist_group("oracle") -async def _drop_table(driver: "OracleAsyncDriver", table_name: str) -> None: - await driver.execute_script( - f"BEGIN EXECUTE IMMEDIATE 'DROP TABLE {table_name}'; " - "EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;" - ) - - +@pytest.mark.xdist_group("oracle_18c") async def test_non_native_json_session_uses_oracle_18c(oracle_18c_async_session: "OracleAsyncDriver") -> None: """The non-native JSON lane must run against the real Oracle 18c service.""" major = await oracle_18c_async_session.select_value( @@ -56,6 +50,7 @@ async def test_native_json_round_trip_matrix(oracle_async_session: "OracleAsyncD await _drop_table(oracle_async_session, table_name) +@pytest.mark.xdist_group("oracle_18c") async def test_blob_is_json_round_trip_on_oracle_18c(oracle_18c_async_session: "OracleAsyncDriver") -> None: """Oracle 18c routes single and array-DML Python JSON through BLOB IS JSON storage.""" table_name = "json_modes_blob" @@ -86,6 +81,7 @@ async def test_blob_is_json_round_trip_on_oracle_18c(oracle_18c_async_session: " await _drop_table(oracle_18c_async_session, table_name) +@pytest.mark.xdist_group("oracle_18c") async def test_clob_is_json_round_trip_on_oracle_18c(oracle_18c_async_session: "OracleAsyncDriver") -> None: """Explicit CLOB storage still receives metadata-driven JSON decoding.""" table_name = "json_modes_clob" @@ -106,3 +102,10 @@ async def test_clob_is_json_round_trip_on_oracle_18c(oracle_18c_async_session: " assert isinstance(row["payload"]["number"], float) finally: await _drop_table(oracle_18c_async_session, table_name) + + +async def _drop_table(driver: "OracleAsyncDriver", table_name: str) -> None: + await driver.execute_script( + f"BEGIN EXECUTE IMMEDIATE 'DROP TABLE {table_name}'; " + "EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;" + ) From 023ae818b9acf2d97eb09fa85ffe334d886c679a Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 18:08:24 +0000 Subject: [PATCH 10/35] chore(test): add cloud storage and pure-go adbc drivers to extras group --- pyproject.toml | 6 + tests/integration/conftest.py | 5 + uv.lock | 219 ++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 328731760..d6b5c1799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,12 @@ extras = [ "adbc_driver_postgresql", "adbc_driver_flightsql", "adbc_driver_bigquery", + "adbc-driver-db2>=0.1.2", + "adbc-driver-gizmosql>=1.1.0", + "adbc-driver-oracle>=0.1.3", + "adbc-driver-snowflake", + "adlfs", + "gcsfs", "dishka", ] lint = [ diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c1ed03218..45d667e69 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -13,15 +13,19 @@ from pytest_databases.docker.rustfs import RustfsService pytest_plugins = [ + "pytest_databases.docker.azure_blob", "pytest_databases.docker.bigquery", "pytest_databases.docker.cockroachdb", "pytest_databases.docker.db2", + "pytest_databases.docker.floci", "pytest_databases.docker.gizmosql", + "pytest_databases.docker.mariadb", "pytest_databases.docker.mssql", "pytest_databases.docker.mysql", "pytest_databases.docker.oracle", "pytest_databases.docker.postgres", "pytest_databases.docker.rustfs", + "pytest_databases.docker.snowflake", "pytest_databases.docker.spanner", ] @@ -31,6 +35,7 @@ "adbc": (("adbc_driver_manager", "pyarrow"),), "adapter_cases": ((),), "bigquery": (("google.cloud.bigquery",),), + "cloud_storage": ((),), "cockroach": (("asyncpg",), ("psycopg", "psycopg_pool")), "db2": (("ibm_db",),), "duckdb": (("duckdb",),), diff --git a/uv.lock b/uv.lock index 268e81a6c..014048af6 100644 --- a/uv.lock +++ b/uv.lock @@ -67,6 +67,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/41/595bdc39a6e8eea2be3ccd043a10e68243b8a682d45f52b5d982674269f3/adbc_driver_bigquery-1.11.0-py3-none-win_amd64.whl", hash = "sha256:75ff98aa9071707419c300f9da74e19da375cd6524ecf09493c8131960e31c3c", size = 17698043, upload-time = "2026-04-07T00:14:53.622Z" }, ] +[[package]] +name = "adbc-driver-db2" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adbc-driver-manager" }, + { name = "importlib-resources" }, + { name = "pyarrow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/48c9619cc81c8cbef39cb9c3253bd91005c84067078d7508ee1047daaf7d/adbc_driver_db2-0.2.1-py3-none-macosx_12_0_universal2.whl", hash = "sha256:3573a21ef6f22ab8b66062d982bbafef99a16ca19fb7069e85dfa3c7a79b4a87", size = 3979102, upload-time = "2026-09-30T16:12:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/da/ff/673cede5fe0a4942526139829e29f1338bdcbb75381989e48c03e7296bda/adbc_driver_db2-0.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:286da0af1d0c64676986792f6331592e9e8c2bde5a151877c46e7961c54204de", size = 7507117, upload-time = "2026-09-30T16:12:34.021Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/6b70086e8873d5d64e961bdcf842d9b64bed2046580b59d56826d789909c/adbc_driver_db2-0.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ccd8eb2ffe39a439ca36e186c38d515defe64227c8e5a295f2ee87f5a3f26b37", size = 8280573, upload-time = "2026-09-30T16:12:36.045Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/6679772b54d968226452cd700b18fa7d09d54fe0e0d254c5615396be9ce4/adbc_driver_db2-0.2.1-py3-none-win_amd64.whl", hash = "sha256:9eed1d3de68d868cef7df835cfd1f5930a882b2455222b6b178e13ebbbdd8aab", size = 8134110, upload-time = "2026-09-30T16:12:38.749Z" }, +] + [[package]] name = "adbc-driver-flightsql" version = "1.12.0" @@ -84,6 +100,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/c7/6773dedc54fad73277ee2d767dd817e203e949972b0cfb197df99e00b784/adbc_driver_flightsql-1.12.0-py3-none-win_amd64.whl", hash = "sha256:098f2498778fd3efd671fbeacba80987de98ebfeb0291da77fe0a26c6fa78532", size = 15033803, upload-time = "2026-07-28T00:41:42.483Z" }, ] +[[package]] +name = "adbc-driver-gizmosql" +version = "2.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adbc-driver-manager" }, + { name = "pyarrow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/c3/d5db1abcea27640316d8bc8c5ef9a8e484141625869506576ba02516046a/adbc_driver_gizmosql-2.0.15-py3-none-macosx_11_0_universal2.whl", hash = "sha256:e2d0d1a9e27531c2b6b1802b4e91c247882b873ae128059a5d27c81cda36d512", size = 7731932, upload-time = "2026-10-15T15:32:07.645Z" }, + { url = "https://files.pythonhosted.org/packages/59/81/c27222e2ca87c32fbd27fbf60dd34a7d6b8f998edfb50c22706570b9b7a2/adbc_driver_gizmosql-2.0.15-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:4db0d79d338633c7851fd9201a13b5d2a348b1a80d5e18a315a0dc9fc7dd94be", size = 8364974, upload-time = "2026-10-15T15:32:09.478Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c6/4657531cd20cfdbc009274afc9732260851923605b65dff14b204ef1db9f/adbc_driver_gizmosql-2.0.15-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4ab0ee877379a183dd57ada85ae597d145664fd820bd1f950bbf881b1efed33", size = 15145743, upload-time = "2026-10-15T15:32:11.565Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/f0bce82c184b0cc210469037ef11bedf3d60eedeb5be8efe3720a1b7c394/adbc_driver_gizmosql-2.0.15-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a53915c959e91ed9a2d9a6460356cc0fd7af84d43a5cc397b11977a88958639", size = 13954800, upload-time = "2026-10-15T15:32:13.927Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/ca80126114b838373d8c0446f354f0330551c42aa28a9aea24b18238c372/adbc_driver_gizmosql-2.0.15-py3-none-win_amd64.whl", hash = "sha256:1d689b38881d5161c733ecf05f1c94bf24fdaea94d4a361e855700088b78734e", size = 15081876, upload-time = "2026-10-15T15:32:16.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/73cd364434e532cd8d7109bef0882c476a8cdeb0c8cc29f426c22d87913a/adbc_driver_gizmosql-2.0.15-py3-none-win_arm64.whl", hash = "sha256:a82135401501f8c4f632059e448249acf9db3d80e13dfec99b167d3d79df04fb", size = 13707197, upload-time = "2026-10-15T15:32:18.354Z" }, +] + [[package]] name = "adbc-driver-manager" version = "1.12.0" @@ -120,6 +153,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/76/6749e0c0c437219780c65487cff67dc09a556c1fccf577a2b27f7b92a704/adbc_driver_manager-1.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cfa08f8c7c63e3fa92eb4e26ef4d8a9520cf92a39281cd011821f6f16a963080", size = 793451, upload-time = "2026-07-28T00:42:39.222Z" }, ] +[[package]] +name = "adbc-driver-oracle" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adbc-driver-manager" }, + { name = "importlib-resources" }, + { name = "pyarrow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b4/1c0a418a4cd99a32ecf6d054d9ef71de869f7c4d83ff10fb6e953281f4fd/adbc_driver_oracle-1.0.1-py3-none-macosx_12_0_universal2.whl", hash = "sha256:4bfaedbb38803893791842601504e45d583ac5e94a75d3ca867e300f9322cf77", size = 4733798, upload-time = "2026-10-15T16:11:06.957Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e2/169358ad85ac78a2558bf9f458b70f1a6cb2397827f3f952e4a5016dff89/adbc_driver_oracle-1.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b9baeca7fdfdd04d1250f0dc2dd2932cad4fc2841bc737d18b5c04d6049c91a4", size = 8833896, upload-time = "2026-10-15T16:11:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a5/747b2bf253c3988a094f3619c7a54a77016a83964dfcabba085c3054e6f3/adbc_driver_oracle-1.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b1008012fc1bcd9efc11a593c06df9fa5fee4bf059ab7bacddb39e96ac3f24d9", size = 9641676, upload-time = "2026-10-15T16:11:11.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/a8/c7a6ba0cac3da4c2a0fdb12ed37105b64c789b20aaafb890408663920ca1/adbc_driver_oracle-1.0.1-py3-none-win_amd64.whl", hash = "sha256:7abc29e759119fafbcd6f59ab37175fcafd767c8ee9835a8fda73ee927a65e53", size = 9598563, upload-time = "2026-10-15T16:11:15.301Z" }, +] + [[package]] name = "adbc-driver-postgresql" version = "1.12.0" @@ -137,6 +186,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/02/7aa782cbb0134b09d1e67757c81332e0cd5e5697beecc8852468482193b0/adbc_driver_postgresql-1.12.0-py3-none-win_amd64.whl", hash = "sha256:5a3b5262eed6f28fb4c782b532e6a65caed1f2268fab7be736335ead49eed9dc", size = 3207946, upload-time = "2026-07-28T00:42:51.543Z" }, ] +[[package]] +name = "adbc-driver-snowflake" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adbc-driver-manager" }, + { name = "importlib-resources" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/c1/561549e36abe8644bedc6b4848abb79ab41a6b0fc4b106ceef944b0c3493/adbc_driver_snowflake-1.11.0.tar.gz", hash = "sha256:7fce54ae7540c6c63aad6cc9ec2795a05647cc147c8af41ca2bd4e95455e8eae", size = 30570, upload-time = "2026-04-07T00:17:30.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/f3/1125c7929faf750e7a5bdcec33ca22259ce040338dae12f75f3eb8080def/adbc_driver_snowflake-1.11.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:5ebddfa5fc0b5cd7c86b5cce17bb578889c8db5ff2b756903b64e684b56829e8", size = 15094744, upload-time = "2026-04-07T00:16:55.86Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1d/88d564a8941607e6e555a3d3290ecb92852d4aa5a926a1cdbe86b17a8424/adbc_driver_snowflake-1.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be862f8d0b6a507dae5c0644e13be79290180532c097d97c5a95a294c0bb5302", size = 13517204, upload-time = "2026-04-07T00:17:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/26/baa44b8b9faaf26846dd8c0e513c7ce6862d7f35de46ee61175ac976699f/adbc_driver_snowflake-1.11.0-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c7d7ecef7246303648a08cee3d78c383133bea8f2e1568cf6e8750ab94b5a57", size = 27574063, upload-time = "2026-04-07T00:17:04.862Z" }, + { url = "https://files.pythonhosted.org/packages/5d/41/fe557f0d8414037dfe86fcba6338eb094a51da894050ec9a4c9f4504513d/adbc_driver_snowflake-1.11.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07b5edbeb1b85be0882d7095641df26a24b3b4c2ba4d8edda35e3bbe89e70a06", size = 24767291, upload-time = "2026-04-07T00:17:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/542793fc9a164f013e3248061e922ac5d5246ce93718a5c0cb44bbd2219e/adbc_driver_snowflake-1.11.0-py3-none-win_amd64.whl", hash = "sha256:83b1a363a44ae4cb1e4956f9cc6c4e44dddd7c768a0f0a8b42d9cb8eb8906305", size = 27264722, upload-time = "2026-04-07T00:17:14.083Z" }, +] + [[package]] name = "adbc-driver-sqlite" version = "1.12.0" @@ -154,6 +220,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/d9/3245d741936100365ea77c434f84a0985523467bd812e5e11bb9b36d7152/adbc_driver_sqlite-1.12.0-py3-none-win_amd64.whl", hash = "sha256:0982bfc06158c2140b5c490b1a1325019c827b158f8432a30d49c8a0c18533ad", size = 1522964, upload-time = "2026-07-28T00:43:00.917Z" }, ] +[[package]] +name = "adlfs" +version = "2026.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "azure-identity" }, + { name = "azure-storage-blob", extra = ["aio"] }, + { name = "fsspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/29/9937905acfaf3e4826d7782f8efab845bb11eaa2e24749a16eeab32e7fa0/adlfs-2026.8.0.tar.gz", hash = "sha256:b78a01bd892c2a99f461b2b2ef4592c2eb45934e728f318472291ccaa6378e06", size = 55898, upload-time = "2026-08-11T18:43:14.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/8c/2a895811f69699915ddd4e346ea3a1ab26b585025eac890d19dd5193c7f4/adlfs-2026.8.0-py3-none-any.whl", hash = "sha256:858d8999b83ad70baae89ee2ad67f40443f9d94e0257351a74866d643f22d54d", size = 46108, upload-time = "2026-08-11T18:43:12.974Z" }, +] + [[package]] name = "aiobotocore" version = "3.9.2" @@ -706,6 +787,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, ] +[package.optional-dependencies] +aio = [ + { name = "aiohttp" }, +] + [[package]] name = "azure-identity" version = "1.26.0" @@ -722,6 +808,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/d5/ae45ce8009d0d015304b206c7c4a6cbab02c03f8f07eedefb32b099ccee3/azure_identity-1.26.0-py3-none-any.whl", hash = "sha256:3c60d9682b3ac01507388d474cb5a5ffbe54152a96b6cdcaa7013da9315cf186", size = 195958, upload-time = "2026-10-01T14:17:49.705Z" }, ] +[[package]] +name = "azure-storage-blob" +version = "12.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/ca/5299cedef5957dd838d4dc46f97bea37335bb39f6610d72b27e1a3317650/azure_storage_blob-12.31.0.tar.gz", hash = "sha256:997b393cfcbdc4b186d5911790d91f80387f7edc12c4d73eab963a2d26e5b2a9", size = 670868, upload-time = "2026-09-30T21:23:22.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/57/d1f45fbccc0dfbe6b6db7e5fa06199e35219c712f3743677bec1b4e7d78b/azure_storage_blob-12.31.0-py3-none-any.whl", hash = "sha256:0c0cb601d3462491d09ea96023cd791bb9dd4b173bf950daf3cff34ff47ba5b5", size = 455738, upload-time = "2026-09-30T21:23:24.944Z" }, +] + +[package.optional-dependencies] +aio = [ + { name = "azure-core", extra = ["aio"] }, +] + [[package]] name = "babel" version = "2.18.0" @@ -1333,6 +1439,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, ] +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + [[package]] name = "deepmerge" version = "3.0.1" @@ -1703,6 +1818,25 @@ s3 = [ { name = "s3fs" }, ] +[[package]] +name = "gcsfs" +version = "2026.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "decorator" }, + { name = "fsspec" }, + { name = "google-auth" }, + { name = "google-auth-oauthlib" }, + { name = "google-cloud-storage" }, + { name = "google-cloud-storage-control" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/78/e3eeed58d314b3906c36896999b5f15bd1e0669fba7a8d6733aee046234c/gcsfs-2026.10.1.tar.gz", hash = "sha256:ebdd435946894c47b17a2e4ac2d5f2832e8cde520e227db65d9b901c01fd7d28", size = 1118233, upload-time = "2026-10-16T02:07:28.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/64/bd0cd661009e1e379a7d5d100f13d40dd0a1a28fa70115931d0e0328f7bd/gcsfs-2026.10.1-py3-none-any.whl", hash = "sha256:037e7e1fa695f12719eeb778be6453085dce0ecd8b17ef1dbeecbbce05fa3b60", size = 88539, upload-time = "2026-10-16T02:07:26.913Z" }, +] + [[package]] name = "google-adk" version = "2.12.0" @@ -1781,6 +1915,19 @@ requests = [ { name = "requests" }, ] +[[package]] +name = "google-auth-oauthlib" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/40/1d7901e454831247e377ef3f642cb857cc0e82ec4d2380b7a148a48f20d1/google_auth_oauthlib-1.5.0.tar.gz", hash = "sha256:b351107c7dd9017f426cbb0272ea1bc04f469020fd18f4443c7d40362e0b1510", size = 23926, upload-time = "2026-09-29T19:25:56.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/bb/b5cc0757ad2f8068e7eb7180c37f82eb488627624c1b1214aa47ddbc032c/google_auth_oauthlib-1.5.0-py3-none-any.whl", hash = "sha256:71625fdea21c6c03217eb9ff13741c3096e6d257ebca0ad084dbd6e9f4aa4ef4", size = 20348, upload-time = "2026-09-29T19:25:31.842Z" }, +] + [[package]] name = "google-cloud-alloydb" version = "0.11.4" @@ -1938,6 +2085,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/77/4376bb26f24966008a9522c61bae025d7030baabf443f20c4b795fb58308/google_cloud_storage-3.17.1-py3-none-any.whl", hash = "sha256:287c7919d7bcda710eae6f6b60c756e89140475d9a4daec01b6174e737488ca8", size = 345706, upload-time = "2026-10-15T17:49:27.497Z" }, ] +[[package]] +name = "google-cloud-storage-control" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/8b/71e457c5442faf669fb23c3b400952d2f2de4d7a5cd576e52275b25365b1/google_cloud_storage_control-1.16.0.tar.gz", hash = "sha256:52af754aa24bee34bc58287f67a1a849b07c33a010d58d2c1f7c91fa1be0f119", size = 160170, upload-time = "2026-10-01T18:18:16.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/6c/c779f90e89a381ba38c3bd4173f1560e0765dd4d2b31ca686f7c90484a64/google_cloud_storage_control-1.16.0-py3-none-any.whl", hash = "sha256:b3b20047d09743fc143db70ac2738f1df7c482af381335bb0dbb86ee4732215d", size = 115201, upload-time = "2026-10-01T18:12:30.057Z" }, +] + [[package]] name = "google-crc32c" version = "1.9.0" @@ -2491,6 +2655,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -3675,6 +3848,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/e5/f5c38d4899cd8edd5b522071c5658202077d113c3225484d779cf257970e/numpydoc-1.11.0-py3-none-any.whl", hash = "sha256:493ad2ce0d8275ca5da484ec37dbdbbc1334c3a6968dffd10ec729b51753e15e", size = 71464, upload-time = "2026-09-15T16:31:07.521Z" }, ] +[[package]] +name = "oauthlib" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/a1bcc8ba112a627f8ffbdc212a78ce18d3ac07e91a5ca65d27918eee25a1/oauthlib-4.0.0.tar.gz", hash = "sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86", size = 187232, upload-time = "2026-09-28T06:01:18.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/f4/78229a1066068ca14fc60fb26cf7381cabe4382261392b90e5f9552722d4/oauthlib-4.0.0-py3-none-any.whl", hash = "sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd", size = 159715, upload-time = "2026-09-28T06:01:17.008Z" }, +] + [[package]] name = "obstore" version = "0.12.0" @@ -5106,6 +5288,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "rich" version = "15.0.0" @@ -5915,10 +6110,15 @@ build = [ ] dev = [ { name = "adbc-driver-bigquery" }, + { name = "adbc-driver-db2" }, { name = "adbc-driver-flightsql" }, + { name = "adbc-driver-gizmosql" }, { name = "adbc-driver-manager" }, + { name = "adbc-driver-oracle" }, { name = "adbc-driver-postgresql" }, + { name = "adbc-driver-snowflake" }, { name = "adbc-driver-sqlite" }, + { name = "adlfs" }, { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, @@ -5930,6 +6130,7 @@ dev = [ { name = "dishka" }, { name = "duckdb-engine" }, { name = "fsspec", extra = ["s3"] }, + { name = "gcsfs" }, { name = "hatch-mypyc" }, { name = "mypy" }, { name = "myst-parser" }, @@ -5994,12 +6195,18 @@ doc = [ ] extras = [ { name = "adbc-driver-bigquery" }, + { name = "adbc-driver-db2" }, { name = "adbc-driver-flightsql" }, + { name = "adbc-driver-gizmosql" }, { name = "adbc-driver-manager" }, + { name = "adbc-driver-oracle" }, { name = "adbc-driver-postgresql" }, + { name = "adbc-driver-snowflake" }, { name = "adbc-driver-sqlite" }, + { name = "adlfs" }, { name = "dishka" }, { name = "fsspec", extra = ["s3"] }, + { name = "gcsfs" }, { name = "pgvector" }, { name = "polars" }, { name = "pyarrow" }, @@ -6114,10 +6321,15 @@ build = [ ] dev = [ { name = "adbc-driver-bigquery" }, + { name = "adbc-driver-db2", specifier = ">=0.1.2" }, { name = "adbc-driver-flightsql" }, + { name = "adbc-driver-gizmosql", specifier = ">=1.1.0" }, { name = "adbc-driver-manager" }, + { name = "adbc-driver-oracle", specifier = ">=0.1.3" }, { name = "adbc-driver-postgresql" }, + { name = "adbc-driver-snowflake" }, { name = "adbc-driver-sqlite" }, + { name = "adlfs" }, { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, @@ -6129,6 +6341,7 @@ dev = [ { name = "dishka" }, { name = "duckdb-engine", specifier = ">=0.17.0" }, { name = "fsspec", extras = ["s3"] }, + { name = "gcsfs" }, { name = "hatch-mypyc" }, { name = "mypy", specifier = ">=2.0.0" }, { name = "myst-parser" }, @@ -6193,12 +6406,18 @@ doc = [ ] extras = [ { name = "adbc-driver-bigquery" }, + { name = "adbc-driver-db2", specifier = ">=0.1.2" }, { name = "adbc-driver-flightsql" }, + { name = "adbc-driver-gizmosql", specifier = ">=1.1.0" }, { name = "adbc-driver-manager" }, + { name = "adbc-driver-oracle", specifier = ">=0.1.3" }, { name = "adbc-driver-postgresql" }, + { name = "adbc-driver-snowflake" }, { name = "adbc-driver-sqlite" }, + { name = "adlfs" }, { name = "dishka" }, { name = "fsspec", extras = ["s3"] }, + { name = "gcsfs" }, { name = "pgvector" }, { name = "polars" }, { name = "pyarrow" }, From 26817a28164f920e78b7ccfaf784c3f0a47c6d04 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 18:43:10 +0000 Subject: [PATCH 11/35] test(oracle): add live adb-free 23ai mtls wallet_zip and dbms_cloud suite --- .../oracle/oracledb/test_adb_wallet.py | 126 ++++++++++++ tests/integration/fixtures/oracle.py | 179 +++++++++++++++++- 2 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 tests/integration/adapters/oracle/oracledb/test_adb_wallet.py diff --git a/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py b/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py new file mode 100644 index 000000000..00698cc19 --- /dev/null +++ b/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py @@ -0,0 +1,126 @@ +"""Live Oracle Autonomous Database Free 23ai mTLS wallet_zip and DBMS_CLOUD integration tests.""" + +import zipfile +from typing import TYPE_CHECKING +from uuid import uuid4 + +import pytest + +from sqlspec.adapters.oracledb import OracleAsyncConfig, OraclePoolParams, OracleSyncConfig +from sqlspec.adapters.oracledb.core import parse_tnsnames_ora +from sqlspec.exceptions import SQLSpecError + +if TYPE_CHECKING: + from pathlib import Path + + from pytest_databases.docker.oracle import OracleADBService + +pytestmark = [ + pytest.mark.oracle, + pytest.mark.integration, + pytest.mark.xdist_group("oracle_adb"), + pytest.mark.timeout(600), +] + + +def test_oracle_adb_sync_wallet_zip_p12_to_pem_mtls( + oracle_adb_sync_config: OracleSyncConfig, oracle_adb_p12_only_wallet_zip: "Path" +) -> None: + """Verify Thin-mode mTLS connection over tcps via PKCS#12-only wallet_zip conversion.""" + with zipfile.ZipFile(oracle_adb_p12_only_wallet_zip, "r") as archive: + member_names = set(archive.namelist()) + assert "ewallet.p12" in member_names + assert "tnsnames.ora" in member_names + assert "sqlnet.ora" in member_names + assert "ewallet.pem" not in member_names + assert "cwallet.sso" not in member_names + + wallet_location = oracle_adb_sync_config.connection_config.get("wallet_location") + assert isinstance(wallet_location, str) + + with oracle_adb_sync_config.provide_session() as driver: + protocol = driver.select_value("SELECT SYS_CONTEXT('USERENV', 'NETWORK_PROTOCOL') AS proto FROM DUAL") + assert str(protocol).lower() == "tcps" + echoed = driver.select_value("SELECT :val AS v FROM DUAL", {"val": "mtls-sync"}) + assert echoed == "mtls-sync" + + +async def test_oracle_adb_async_wallet_zip_p12_to_pem_mtls(oracle_adb_async_config: OracleAsyncConfig) -> None: + """Verify async Thin-mode mTLS connection and parameter binding via wallet_zip.""" + async with oracle_adb_async_config.provide_session() as driver: + protocol = await driver.select_value("SELECT SYS_CONTEXT('USERENV', 'NETWORK_PROTOCOL') AS proto FROM DUAL") + assert str(protocol).lower() == "tcps" + echoed = await driver.select_value("SELECT :val AS v FROM DUAL", {"val": "mtls-async"}) + assert echoed == "mtls-async" + + +def test_oracle_adb_extracted_wallet_dir_service_name( + oracle_adb_service: "OracleADBService", oracle_adb_wallet_dir: "Path" +) -> None: + """Verify direct extracted wallet directory connection using TNS alias resolution.""" + tns_alias = _resolve_mtls_alias(oracle_adb_service, oracle_adb_wallet_dir) + config = OracleSyncConfig( + connection_config=OraclePoolParams( + dsn=tns_alias, + user=oracle_adb_service.user, + password=oracle_adb_service.password, + config_dir=str(oracle_adb_wallet_dir), + wallet_location=str(oracle_adb_wallet_dir), + wallet_password=oracle_adb_service.wallet_password, + min=1, + max=2, + ) + ) + try: + with config.provide_session() as driver: + protocol = driver.select_value("SELECT SYS_CONTEXT('USERENV', 'NETWORK_PROTOCOL') AS proto FROM DUAL") + assert str(protocol).lower() == "tcps" + finally: + config.close_pool() + + +def test_oracle_adb_dbms_cloud_capability_probe(oracle_adb_sync_config: OracleSyncConfig) -> None: + """Verify DBMS_CLOUD catalog discovery and SQLSpecError mapping on COPY_DATA invocation.""" + table_name = f"adb_cloud_probe_{uuid4().hex[:8]}" + with oracle_adb_sync_config.provide_session() as driver: + package_count = int( + driver.select_value( + "SELECT COUNT(*) AS cnt FROM ALL_OBJECTS WHERE OBJECT_NAME = 'DBMS_CLOUD' AND OBJECT_TYPE = 'PACKAGE'" + ) + or 0 + ) + assert package_count >= 0 + driver.execute(f"CREATE TABLE {table_name} (id NUMBER, label VARCHAR2(64))") + try: + with pytest.raises(SQLSpecError) as exc_info: + driver.load_from_storage( + table_name, + "https://objectstorage.us-ashburn-1.oraclecloud.com/n/testns/b/testbkt/o/probe.parquet", + file_format="parquet", + credential_name="SQLSPEC_NONEXISTENT_CRED", + ) + error_text = str(exc_info.value).upper() + if package_count > 0: + assert "ORA-" in error_text + else: + assert "DBMS_CLOUD" in error_text or "ORA-" in error_text or "PLS-" in error_text + finally: + driver.execute(f"DROP TABLE {table_name} PURGE") + + +def _resolve_mtls_alias(oracle_adb_service: "OracleADBService", wallet_dir: "Path") -> str: + """Select an mTLS TCPS alias from the extracted ADB wallet tnsnames.ora.""" + entries = parse_tnsnames_ora(wallet_dir / "tnsnames.ora") + candidates = ( + oracle_adb_service.service_name.lower(), + oracle_adb_service.service_name.lower().split(".", 1)[0], + *(alias.lower() for alias in oracle_adb_service.service_names), + ) + for candidate in candidates: + descriptor = entries.get(candidate, "") + if "tcps" in descriptor.lower(): + return candidate + for alias, descriptor in entries.items(): + if "tcps" in descriptor.lower(): + return alias + return oracle_adb_service.service_names[0] diff --git a/tests/integration/fixtures/oracle.py b/tests/integration/fixtures/oracle.py index 83ef92ffb..f2cc27469 100644 --- a/tests/integration/fixtures/oracle.py +++ b/tests/integration/fixtures/oracle.py @@ -1,16 +1,21 @@ """Shared Oracle integration fixtures.""" import shlex -from typing import TYPE_CHECKING, cast +import time +import zipfile +from typing import TYPE_CHECKING, Any, cast +from uuid import uuid4 import pytest -from pytest_databases.docker.oracle import OracleService +from pytest_databases._service import get_docker_client, should_keep_containers +from pytest_databases.docker.oracle import OracleADBService, OracleService from pytest_databases.helpers import exec_output_to_bytes -from sqlspec.utils.module_loader import import_optional_attr +from sqlspec.utils.module_loader import import_optional, import_optional_attr if TYPE_CHECKING: from collections.abc import AsyncGenerator, Generator + from pathlib import Path from sqlspec.adapters.oracledb import ( OracleAsyncConfig, @@ -19,17 +24,23 @@ OracleSyncConfig, OracleSyncDriver, ) + from sqlspec.adapters.oracledb.core import parse_tnsnames_ora else: OracleAsyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncConfig") OracleAsyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleAsyncDriver") OraclePoolParams = import_optional_attr("sqlspec.adapters.oracledb", "OraclePoolParams") OracleSyncConfig = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncConfig") OracleSyncDriver = import_optional_attr("sqlspec.adapters.oracledb", "OracleSyncDriver") + parse_tnsnames_ora = import_optional_attr("sqlspec.adapters.oracledb.core", "parse_tnsnames_ora") __all__ = ( "oracle_18c_async_config", "oracle_18c_async_session", "oracle_18c_connection_config", + "oracle_adb_async_config", + "oracle_adb_p12_only_wallet_zip", + "oracle_adb_sync_config", + "oracle_adb_volumes", "oracle_aq_privileges", "oracle_async_config", "oracle_async_session", @@ -142,6 +153,87 @@ async def oracle_async_session(oracle_async_config: "OracleAsyncConfig") -> "Asy yield driver +@pytest.fixture(scope="session") +def oracle_adb_volumes(oracle_adb_image: str, request: pytest.FixtureRequest) -> dict[str, dict[str, str]]: + """Patch Oracle ADB Free spfilePOD1.ora filesystemio_options=NONE for overlayfs and 4Kn block devices.""" + dbs_dir = "/u01/app/oracle/product/23.0.0.0/dbhome_1/dbs" + volume_name = f"sqlspec_adb_dbs_{uuid4().hex[:8]}" + client = get_docker_client() + volume = client.volumes.create(name=volume_name) + mounts = {volume_name: {"bind": dbs_dir, "mode": "rw"}} + patch_script = ( + "sqlplus -S / as sysdba <<'EOF'\n" + "CREATE PFILE='/tmp/initPOD1.ora' FROM SPFILE;\n" + "HOST sed -i \"s/filesystemio_options=.*/filesystemio_options='NONE'/\" /tmp/initPOD1.ora\n" + "CREATE SPFILE FROM PFILE='/tmp/initPOD1.ora';\n" + "EXIT;\n" + "EOF" + ) + try: + client.containers.run( + oracle_adb_image, command=["bash", "-lc", patch_script], entrypoint=[], remove=True, volumes=mounts + ) + except Exception: + _remove_adb_volumes([volume]) + client.close() + raise + + def cleanup() -> None: + try: + if not should_keep_containers(request.config): + _remove_adb_volumes([volume]) + finally: + client.close() + + request.config.add_cleanup(cleanup) + return mounts + + +@pytest.fixture(scope="session") +def oracle_adb_p12_only_wallet_zip( + oracle_adb_wallet_dir: "Path", oracle_adb_service: OracleADBService, tmp_path_factory: pytest.TempPathFactory +) -> "Path": + """Build a PKCS#12-only Oracle Autonomous Database wallet archive omitting ewallet.pem and cwallet.sso.""" + archive_dir = tmp_path_factory.mktemp("oracle_adb_wallet_zip") + archive_path = archive_dir / "wallet.zip" + p12_bytes = _load_or_synthesize_adb_p12_bytes(oracle_adb_wallet_dir, oracle_adb_service.wallet_password) + tnsnames_bytes = (oracle_adb_wallet_dir / "tnsnames.ora").read_bytes() + sqlnet_bytes = (oracle_adb_wallet_dir / "sqlnet.ora").read_bytes() + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + archive.writestr("ewallet.p12", p12_bytes) + archive.writestr("tnsnames.ora", tnsnames_bytes) + archive.writestr("sqlnet.ora", sqlnet_bytes) + return archive_path + + +@pytest.fixture(scope="session") +def oracle_adb_sync_config( + oracle_adb_service: OracleADBService, oracle_adb_wallet_dir: "Path", oracle_adb_p12_only_wallet_zip: "Path" +) -> "Generator[OracleSyncConfig, None, None]": + """Provide a session-scoped Oracle Autonomous Database sync config using wallet_zip.""" + pytest.importorskip("oracledb") + params = _oracle_adb_pool_params(oracle_adb_service, oracle_adb_wallet_dir, oracle_adb_p12_only_wallet_zip) + config = OracleSyncConfig(connection_config=OraclePoolParams(**params)) + try: + yield config + finally: + config.close_pool() + + +@pytest.fixture(scope="session") +async def oracle_adb_async_config( + oracle_adb_service: OracleADBService, oracle_adb_wallet_dir: "Path", oracle_adb_p12_only_wallet_zip: "Path" +) -> "AsyncGenerator[OracleAsyncConfig, None]": + """Provide a session-scoped Oracle Autonomous Database async config using wallet_zip.""" + pytest.importorskip("oracledb") + params = _oracle_adb_pool_params(oracle_adb_service, oracle_adb_wallet_dir, oracle_adb_p12_only_wallet_zip) + config = OracleAsyncConfig(connection_config=OraclePoolParams(**params)) + try: + yield config + finally: + await config.close_pool() + + def _oracle_pool_params(oracle_service: OracleService) -> "OraclePoolParams": return cast( "OraclePoolParams", @@ -157,3 +249,84 @@ def _oracle_pool_params(oracle_service: OracleService) -> "OraclePoolParams": "retry_delay": 1, }, ) + + +def _oracle_adb_pool_params( + oracle_adb_service: OracleADBService, wallet_dir: "Path", wallet_zip_path: "Path" +) -> "OraclePoolParams": + tns_alias = _resolve_oracle_adb_mtls_alias(oracle_adb_service, wallet_dir) + return cast( + "OraclePoolParams", + { + "dsn": tns_alias, + "user": oracle_adb_service.user, + "password": oracle_adb_service.password, + "wallet_zip": wallet_zip_path, + "wallet_password": oracle_adb_service.wallet_password, + "min": 1, + "max": 3, + "retry_count": 5, + "retry_delay": 1, + }, + ) + + +def _resolve_oracle_adb_mtls_alias(oracle_adb_service: OracleADBService, wallet_dir: "Path") -> str: + entries = parse_tnsnames_ora(wallet_dir / "tnsnames.ora") + candidates = ( + oracle_adb_service.service_name.lower(), + oracle_adb_service.service_name.lower().split(".", 1)[0], + *(alias.lower() for alias in oracle_adb_service.service_names), + ) + for candidate in candidates: + descriptor = entries.get(candidate, "") + if "tcps" in descriptor.lower(): + return candidate + for alias, descriptor in entries.items(): + if "tcps" in descriptor.lower(): + return alias + return oracle_adb_service.service_names[0] + + +def _load_or_synthesize_adb_p12_bytes(wallet_dir: "Path", wallet_password: str) -> bytes: + p12_path = wallet_dir / "ewallet.p12" + if p12_path.is_file(): + return p12_path.read_bytes() + pem_path = wallet_dir / "ewallet.pem" + if not pem_path.is_file(): + msg = f"Expected ewallet.p12 or ewallet.pem in {wallet_dir}" + raise RuntimeError(msg) + serialization_mod: Any = import_optional("cryptography.hazmat.primitives.serialization") + pkcs12_mod: Any = import_optional("cryptography.hazmat.primitives.serialization.pkcs12") + x509_mod: Any = import_optional("cryptography.x509") + if serialization_mod is None or pkcs12_mod is None or x509_mod is None: + msg = "cryptography is required to synthesize ewallet.p12 from ewallet.pem" + raise RuntimeError(msg) + pem_bytes = pem_path.read_bytes() + password_bytes = wallet_password.encode("utf-8") + try: + private_key = serialization_mod.load_pem_private_key(pem_bytes, password=None) + except TypeError: + private_key = serialization_mod.load_pem_private_key(pem_bytes, password=password_bytes) + certs = x509_mod.load_pem_x509_certificates(pem_bytes) + leaf_cert = certs[0] if certs else None + ca_certs = certs[1:] if len(certs) > 1 else None + synthesized: bytes = pkcs12_mod.serialize_key_and_certificates( + name=b"oracle-adb-wallet", + key=private_key, + cert=leaf_cert, + cas=ca_certs, + encryption_algorithm=serialization_mod.BestAvailableEncryption(password_bytes), + ) + return synthesized + + +def _remove_adb_volumes(volumes: list[Any]) -> None: + """Remove temporary Docker volumes created for Oracle ADB Free O_DIRECT directories.""" + for volume in volumes: + for _ in range(10): + try: + volume.remove(force=True) + break + except Exception: + time.sleep(0.5) From e17ef7d686aa3a4676fa66b794d7a851b7506e81 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sun, 25 Oct 2026 19:12:25 +0000 Subject: [PATCH 12/35] test(storage): add multi-cloud floci/azurite storage, streaming, and spanner adk artifact suites --- .../extensions/adk/test_adk_artifact_store.py | 175 ++++++++ tests/integration/fixtures/cloud_storage.py | 323 ++++++++++++++ tests/integration/storage/test_integration.py | 410 ++++++++---------- tests/integration/storage/test_streaming.py | 221 ++++++++-- 4 files changed, 859 insertions(+), 270 deletions(-) create mode 100644 tests/integration/adapters/spanner/spanner/extensions/adk/test_adk_artifact_store.py create mode 100644 tests/integration/fixtures/cloud_storage.py diff --git a/tests/integration/adapters/spanner/spanner/extensions/adk/test_adk_artifact_store.py b/tests/integration/adapters/spanner/spanner/extensions/adk/test_adk_artifact_store.py new file mode 100644 index 000000000..89c0264a6 --- /dev/null +++ b/tests/integration/adapters/spanner/spanner/extensions/adk/test_adk_artifact_store.py @@ -0,0 +1,175 @@ +"""Live Spanner + Floci GCS Google ADK artifact store integration tests.""" + +from collections.abc import Awaitable +from inspect import isawaitable +from typing import TYPE_CHECKING, Any, TypeVar +from uuid import uuid4 + +import pytest + +from sqlspec.storage.registry import StorageRegistry +from sqlspec.utils.module_loader import import_optional, import_optional_attr +from tests.integration.fixtures.cloud_storage import floci_gcs_obstore_kwargs + +if TYPE_CHECKING: + from google.cloud.spanner_v1.database import Database + from google.genai import types + from pytest_databases.docker.floci import FlociGCPService + from pytest_databases.docker.spanner import SpannerService + + from sqlspec.adapters.spanner import SpannerAsyncConfig, SpannerSyncConfig + from sqlspec.adapters.spanner.adk.artifact_store import SpannerAsyncADKArtifactStore, SpannerSyncADKArtifactStore + from sqlspec.extensions.adk.artifact.service import SQLSpecArtifactService + from sqlspec.protocols import ObjectStoreProtocol +else: + types = import_optional("google.genai.types") + SpannerAsyncConfig = import_optional_attr("sqlspec.adapters.spanner", "SpannerAsyncConfig") + SpannerSyncConfig = import_optional_attr("sqlspec.adapters.spanner", "SpannerSyncConfig") + SpannerAsyncADKArtifactStore = import_optional_attr( + "sqlspec.adapters.spanner.adk.artifact_store", "SpannerAsyncADKArtifactStore" + ) + SpannerSyncADKArtifactStore = import_optional_attr( + "sqlspec.adapters.spanner.adk.artifact_store", "SpannerSyncADKArtifactStore" + ) + SQLSpecArtifactService = import_optional_attr("sqlspec.extensions.adk.artifact.service", "SQLSpecArtifactService") + +build_spanner_connection_config: Any = import_optional_attr( + "tests.integration.fixtures.spanner", "build_spanner_connection_config" +) + +pytest.importorskip("google.cloud.spanner") +pytest.importorskip("google.adk") +pytest.importorskip("google.genai") +pytest.importorskip("obstore") + +pytestmark = [pytest.mark.spanner, pytest.mark.integration, pytest.mark.xdist_group("spanner")] + +T = TypeVar("T") + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +async def test_spanner_adk_artifact_service_gcs_round_trip( + mode: str, + spanner_service: "SpannerService", + spanner_database: "Database", + floci_gcp_service: "FlociGCPService", + floci_gcs_bucket_name: str, + floci_gcs_obstore: "ObjectStoreProtocol", +) -> None: + """Verify SQLSpecArtifactService with SpannerSyncADKArtifactStore and SpannerAsyncADKArtifactStore over Floci GCS.""" + del spanner_database + suffix = uuid4().hex[:8] + connection_config = build_spanner_connection_config(spanner_service) + extension_config: dict[str, Any] = {"adk": {"artifact_table": f"adk_art_{suffix}"}} + + config: SpannerSyncConfig | SpannerAsyncConfig + store: SpannerSyncADKArtifactStore | SpannerAsyncADKArtifactStore + if mode == "sync": + config = SpannerSyncConfig(connection_config=connection_config, extension_config=extension_config) + store = SpannerSyncADKArtifactStore(config) + else: + config = SpannerAsyncConfig(connection_config=connection_config, extension_config=extension_config) + store = SpannerAsyncADKArtifactStore(config) + + registry = StorageRegistry() + registry.register_alias( + "test-gcs-artifacts", + f"gs://{floci_gcs_bucket_name}", + backend="obstore", + **floci_gcs_obstore_kwargs(floci_gcp_service), + ) + service = SQLSpecArtifactService(store, "test-gcs-artifacts", registry=registry) + + try: + await _resolve(store.create_table()) + + v0_part = types.Part.from_bytes(data=b"report-v0-payload", mime_type="text/plain") + v1_part = types.Part.from_bytes(data=b"report-v1-payload", mime_type="text/plain") + user_part = types.Part.from_bytes(data=b'{"theme":"dark"}', mime_type="application/json") + + v0 = await service.save_artifact( + app_name="app1", + user_id="u1", + session_id="s1", + filename="report.txt", + artifact=v0_part, + custom_metadata={"stage": "draft"}, + ) + v1 = await service.save_artifact( + app_name="app1", + user_id="u1", + session_id="s1", + filename="report.txt", + artifact=v1_part, + custom_metadata={"stage": "final"}, + ) + user_v0 = await service.save_artifact( + app_name="app1", user_id="u1", session_id="s1", filename="user:prefs.json", artifact=user_part + ) + assert (v0, v1, user_v0) == (0, 1, 0) + + loaded_latest = await service.load_artifact( + app_name="app1", user_id="u1", session_id="s1", filename="report.txt" + ) + assert loaded_latest is not None + assert loaded_latest.inline_data is not None + assert loaded_latest.inline_data.data == b"report-v1-payload" + + loaded_v0 = await service.load_artifact( + app_name="app1", user_id="u1", session_id="s1", filename="report.txt", version=0 + ) + assert loaded_v0 is not None + assert loaded_v0.inline_data is not None + assert loaded_v0.inline_data.data == b"report-v0-payload" + + loaded_user = await service.load_artifact(app_name="app1", user_id="u1", filename="user:prefs.json") + assert loaded_user is not None + assert loaded_user.inline_data is not None + assert loaded_user.inline_data.data == b'{"theme":"dark"}' + + session_keys = await service.list_artifact_keys(app_name="app1", user_id="u1", session_id="s1") + assert set(session_keys) == {"report.txt", "user:prefs.json"} + user_only_keys = await service.list_artifact_keys(app_name="app1", user_id="u1") + assert user_only_keys == ["user:prefs.json"] + + versions = await service.list_versions(app_name="app1", user_id="u1", session_id="s1", filename="report.txt") + assert versions == [0, 1] + + v0_meta = await service.get_artifact_version( + app_name="app1", user_id="u1", session_id="s1", filename="report.txt", version=0 + ) + v1_meta = await service.get_artifact_version( + app_name="app1", user_id="u1", session_id="s1", filename="report.txt", version=1 + ) + assert v0_meta is not None + assert v1_meta is not None + assert v0_meta.canonical_uri.startswith("test-gcs-artifacts/apps/") + assert v1_meta.canonical_uri.startswith("test-gcs-artifacts/apps/") + assert v1_meta.custom_metadata == {"stage": "final"} + + v0_content_path = v0_meta.canonical_uri.removeprefix("test-gcs-artifacts/") + v1_content_path = v1_meta.canonical_uri.removeprefix("test-gcs-artifacts/") + assert floci_gcs_obstore.exists_sync(v0_content_path) is True + assert floci_gcs_obstore.exists_sync(v1_content_path) is True + + await service.delete_artifact(app_name="app1", user_id="u1", session_id="s1", filename="report.txt") + assert ( + await service.load_artifact(app_name="app1", user_id="u1", session_id="s1", filename="report.txt") is None + ) + assert floci_gcs_obstore.exists_sync(v0_content_path) is False + assert floci_gcs_obstore.exists_sync(v1_content_path) is False + + await service.delete_artifact(app_name="app1", user_id="u1", filename="user:prefs.json") + assert await service.load_artifact(app_name="app1", user_id="u1", filename="user:prefs.json") is None + finally: + try: + await _resolve(store.drop_table()) + finally: + await _resolve(config.close_pool()) + + +async def _resolve(result: "T | Awaitable[T]") -> T: + """Await a store/config result when async or return directly when sync.""" + if isawaitable(result): + return await result + return result diff --git a/tests/integration/fixtures/cloud_storage.py b/tests/integration/fixtures/cloud_storage.py new file mode 100644 index 000000000..67c21bf5c --- /dev/null +++ b/tests/integration/fixtures/cloud_storage.py @@ -0,0 +1,323 @@ +"""Clientless multi-cloud object storage integration fixtures for Floci GCS, Floci AWS S3, and Azurite.""" + +import contextlib +import email.utils +import http.client +import http.server +import os +import threading +import time +import urllib.error +import urllib.parse +import urllib.request +from typing import TYPE_CHECKING, Any, ClassVar + +import pytest +from pytest_databases.docker.floci import FlociGCPService +from pytest_databases.helpers import get_xdist_worker_num + +from sqlspec.utils.module_loader import import_optional, import_optional_attr +from sqlspec.utils.serializers import from_json +from tests.fixtures.rustfs import ensure_s3_bucket + +os.environ.setdefault("GCSFS_EXPERIMENTAL_ZB_HNS_SUPPORT", "false") + +if TYPE_CHECKING: + from collections.abc import Generator + + from pytest_databases import ContainerService + from pytest_databases.docker.azure_blob import AzureBlobService + from pytest_databases.docker.floci import FlociAWSService + from pytest_databases.types import ServiceContainer, XdistIsolationLevel + + from sqlspec.protocols import ObjectStoreProtocol + from sqlspec.storage.backends.fsspec import FSSpecBackend + from sqlspec.storage.backends.obstore import ObStoreBackend +else: + FSSpecBackend = import_optional_attr("sqlspec.storage.backends.fsspec", "FSSpecBackend") + ObStoreBackend = import_optional_attr("sqlspec.storage.backends.obstore", "ObStoreBackend") + +fsspec_module: Any = import_optional("fsspec") +CoreGCSFileSystem: Any = import_optional_attr("gcsfs.core", "GCSFileSystem") + +FLOCI_GCP_GATEWAY_PORT = 4588 + +__all__ = ( + "FLOCI_GCP_GATEWAY_PORT", + "azurite_fsspec", + "azurite_obstore", + "floci_aws_s3_bucket_name", + "floci_aws_s3_obstore", + "floci_gcp_service", + "floci_gcs_bucket_name", + "floci_gcs_fsspec", + "floci_gcs_obstore", + "floci_gcs_obstore_kwargs", +) + + +def floci_gcs_obstore_kwargs(floci_gcp_service: "FlociGCPService") -> "dict[str, Any]": + """Build ObStoreBackend keyword arguments for the Floci GCP Storage emulator without host ADC lookup.""" + return { + "base_url": floci_gcp_service.endpoint_url, + "bearer_token": "anon", + "skip_signature": True, + "client_options": {"allow_http": True}, + } + + +@pytest.fixture(scope="session") +def floci_gcp_service( + container_service: "ContainerService", + floci_gcp_image: str, + floci_gcp_project: str, + floci_resource_prefix: str, + xdist_floci_isolation_level: "XdistIsolationLevel", +) -> "Generator[FlociGCPService, None, None]": + """Start and yield a ready Floci GCP emulator service container on gateway port 4588.""" + worker_num = get_xdist_worker_num() + container_name = ( + f"floci_gcp_{worker_num}" if worker_num is not None and xdist_floci_isolation_level == "server" else "floci_gcp" + ) + transient = worker_num is not None and xdist_floci_isolation_level == "server" + with ( + container_service.run( + image=floci_gcp_image, + name=container_name, + container_port=FLOCI_GCP_GATEWAY_PORT, + check=_is_floci_gcp_ready, + timeout=60, + pause=0.25, + transient=transient, + ) as service, + _provide_floci_gcs_header_proxy(service.host, service.port) as proxy_port, + ): + yield FlociGCPService( + container=service.container, + host="127.0.0.1", + port=proxy_port, + endpoint_url=f"http://127.0.0.1:{proxy_port}", + resource_prefix=floci_resource_prefix, + project=floci_gcp_project, + ) + + +@pytest.fixture(scope="session") +def floci_gcs_bucket_name(floci_gcp_service: "FlociGCPService", floci_resource_prefix: str) -> str: + """Ensure and return a worker-isolated GCS bucket name on the Floci GCP emulator.""" + bucket_name = f"{floci_resource_prefix}gcs" + floci_gcp_service.ensure_bucket(bucket_name) + return bucket_name + + +@pytest.fixture +def floci_gcs_obstore(floci_gcp_service: "FlociGCPService", floci_gcs_bucket_name: str) -> "ObjectStoreProtocol": + """Provide an ObStoreBackend connected to the Floci GCP Storage emulator.""" + pytest.importorskip("obstore") + return ObStoreBackend(f"gs://{floci_gcs_bucket_name}", **floci_gcs_obstore_kwargs(floci_gcp_service)) + + +@pytest.fixture +def floci_gcs_fsspec(floci_gcp_service: "FlociGCPService", floci_gcs_bucket_name: str) -> "ObjectStoreProtocol": + """Provide an FSSpecBackend connected to the Floci GCP Storage emulator.""" + pytest.importorskip("fsspec") + pytest.importorskip("gcsfs") + if fsspec_module is not None and CoreGCSFileSystem is not None: + fsspec_module.register_implementation("gs", CoreGCSFileSystem, clobber=True) + fsspec_module.register_implementation("gcs", CoreGCSFileSystem, clobber=True) + return FSSpecBackend(f"gs://{floci_gcs_bucket_name}", token="anon", endpoint_url=floci_gcp_service.endpoint_url) + + +@pytest.fixture(scope="session") +def floci_aws_s3_bucket_name(floci_aws_service: "FlociAWSService", floci_resource_prefix: str) -> str: + """Ensure and return a worker-isolated S3 bucket name on the Floci AWS emulator.""" + bucket_name = f"{floci_resource_prefix}s3" + return ensure_s3_bucket( + floci_aws_service.endpoint_url, + bucket_name, + access_key=floci_aws_service.access_key, + secret_key=floci_aws_service.secret_key, + region=floci_aws_service.region, + ) + + +@pytest.fixture +def floci_aws_s3_obstore(floci_aws_service: "FlociAWSService", floci_aws_s3_bucket_name: str) -> "ObjectStoreProtocol": + """Provide an ObStoreBackend connected to the Floci AWS S3 emulator.""" + pytest.importorskip("obstore") + return ObStoreBackend( + f"s3://{floci_aws_s3_bucket_name}", + aws_endpoint=floci_aws_service.endpoint_url, + aws_access_key_id=floci_aws_service.access_key, + aws_secret_access_key=floci_aws_service.secret_key, + aws_region=floci_aws_service.region, + aws_virtual_hosted_style_request=False, + client_options={"allow_http": True}, + ) + + +@pytest.fixture +def azurite_obstore( + azure_blob_service: "AzureBlobService", azure_blob_default_container_name: str +) -> "ObjectStoreProtocol": + """Provide an ObStoreBackend connected to the Azurite Blob Storage emulator.""" + pytest.importorskip("obstore") + return ObStoreBackend( + f"az://{azure_blob_default_container_name}", + account_name=azure_blob_service.account_name, + account_key=azure_blob_service.account_key, + endpoint=azure_blob_service.account_url, + client_options={"allow_http": True}, + ) + + +@pytest.fixture +def azurite_fsspec( + azure_blob_service: "AzureBlobService", azure_blob_default_container_name: str +) -> "ObjectStoreProtocol": + """Provide an FSSpecBackend connected to the Azurite Blob Storage emulator.""" + pytest.importorskip("fsspec") + pytest.importorskip("adlfs") + return FSSpecBackend( + f"az://{azure_blob_default_container_name}", + account_name=azure_blob_service.account_name, + account_key=azure_blob_service.account_key, + connection_string=azure_blob_service.connection_string, + ) + + +def _is_floci_gcp_ready(service: "ServiceContainer") -> bool: + """Return True when the Floci GCP HTTP health endpoint responds with HTTP 200.""" + for path in ("/_floci-gcp/health", "/_floci/health", "/_localstack/health"): + request = urllib.request.Request(f"http://{service.host}:{service.port}{path}", method="GET") + try: + with urllib.request.urlopen(request, timeout=2.0) as response: + if response.status == 200: + return True + except (urllib.error.URLError, TimeoutError, OSError): + continue + return False + + +class _FlociGCSHeaderProxyHandler(http.server.BaseHTTPRequestHandler): + """Forward HTTP requests to Floci GCP while completing ETag and Last-Modified XML headers for obstore.""" + + upstream_host: ClassVar[str] = "127.0.0.1" + upstream_port: ClassVar[int] = FLOCI_GCP_GATEWAY_PORT + + def do_GET(self) -> None: + """Forward GET requests.""" + self._forward_request() + + def do_HEAD(self) -> None: + """Forward HEAD requests.""" + self._forward_request() + + def do_POST(self) -> None: + """Forward POST requests.""" + self._forward_request() + + def do_PUT(self) -> None: + """Forward PUT requests.""" + self._forward_request() + + def do_DELETE(self) -> None: + """Forward DELETE requests.""" + self._forward_request() + + def do_PATCH(self) -> None: + """Forward PATCH requests.""" + self._forward_request() + + def log_message(self, format: str, *args: object) -> None: + """Suppress per-request stderr logging during test runs.""" + del format, args + + def _forward_request(self) -> None: + content_length = int(self.headers.get("Content-Length", "0")) + request_body = self.rfile.read(content_length) if content_length > 0 else None + upstream_method, upstream_path, forwarded_headers = self._translate_request() + connection = http.client.HTTPConnection(self.upstream_host, self.upstream_port, timeout=30) + try: + connection.request(upstream_method, upstream_path, body=request_body, headers=forwarded_headers) + upstream_response = connection.getresponse() + response_body = upstream_response.read() + response_headers = list(upstream_response.getheaders()) + status_code = upstream_response.status + finally: + connection.close() + + lower_header_names = {key.lower() for key, _ in response_headers} + self.send_response(status_code) + for key, value in response_headers: + if key.lower() in {"connection", "content-length", "date", "server", "transfer-encoding"}: + continue + self.send_header(key, value) + + if 200 <= status_code < 300: + if "etag" not in lower_header_names: + self.send_header("ETag", self._extract_etag(response_body)) + if "last-modified" not in lower_header_names: + self.send_header("Last-Modified", self._format_last_modified(response_headers)) + + self.send_header("Content-Length", str(len(response_body))) + self.end_headers() + if self.command != "HEAD": + self.wfile.write(response_body) + + def _translate_request(self) -> "tuple[str, str, dict[str, str]]": + excluded_headers = {"connection", "transfer-encoding"} + copy_source = self.headers.get("x-goog-copy-source") + if self.command == "PUT" and copy_source is not None: + src_parts = urllib.parse.unquote(copy_source.lstrip("/")).split("/", 1) + dest_parts = urllib.parse.unquote(urllib.parse.urlsplit(self.path).path.lstrip("/")).split("/", 1) + if len(src_parts) == 2 and len(dest_parts) == 2: + src_bucket, src_key = src_parts + dest_bucket, dest_key = dest_parts + copy_path = ( + f"/storage/v1/b/{urllib.parse.quote(src_bucket, safe='')}" + f"/o/{urllib.parse.quote(src_key, safe='')}" + f"/copyTo/b/{urllib.parse.quote(dest_bucket, safe='')}" + f"/o/{urllib.parse.quote(dest_key, safe='')}" + ) + excluded_headers = {"connection", "content-length", "transfer-encoding", "x-goog-copy-source"} + headers = {key: val for key, val in self.headers.items() if key.lower() not in excluded_headers} + headers["Content-Length"] = "0" + return "POST", copy_path, headers + headers = {key: val for key, val in self.headers.items() if key.lower() not in excluded_headers} + return self.command, self.path, headers + + @staticmethod + def _extract_etag(response_body: bytes) -> str: + if response_body.startswith(b"{"): + with contextlib.suppress(Exception): + parsed = from_json(response_body.decode("utf-8", errors="replace")) + if isinstance(parsed, dict) and isinstance(parsed.get("etag"), str): + return f'"{parsed["etag"]}"' + return '"floci"' + + @staticmethod + def _format_last_modified(response_headers: "list[tuple[str, str]]") -> str: + generation = next((val for key, val in response_headers if key.lower() == "x-goog-generation"), None) + timestamp = int(generation) / 1000.0 if generation is not None and generation.isdigit() else time.time() + return email.utils.formatdate(timestamp, usegmt=True) + + +@contextlib.contextmanager +def _provide_floci_gcs_header_proxy(upstream_host: str, upstream_port: int) -> "Generator[int, None, None]": + """Run a background ThreadingHTTPServer normalizing Floci GCP XML headers for obstore compatibility.""" + handler_cls = type( + "_BoundFlociGCSHeaderProxyHandler", + (_FlociGCSHeaderProxyHandler,), + {"upstream_host": upstream_host, "upstream_port": upstream_port}, + ) + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler_cls) + proxy_port = int(server.server_address[1]) + thread = threading.Thread(target=server.serve_forever, name="floci-gcs-header-proxy", daemon=True) + thread.start() + try: + yield proxy_port + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2.0) diff --git a/tests/integration/storage/test_integration.py b/tests/integration/storage/test_integration.py index f991037c9..159ae54cc 100644 --- a/tests/integration/storage/test_integration.py +++ b/tests/integration/storage/test_integration.py @@ -1,42 +1,37 @@ -"""Integration tests for storage backends using RustFS fixtures. - -Tests storage backend operations against S3-compatible storage using pytest-databases RustFS fixtures. -Follows Advanced Alchemy patterns for comprehensive storage testing. -""" +"""Integration tests for storage backends across local, RustFS S3, Floci GCS, Floci AWS S3, and Azurite.""" +import asyncio import logging -from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest -from pytest_databases.docker.rustfs import RustfsService from sqlspec.exceptions import FileNotFoundInStorageError -from sqlspec.protocols import ObjectStoreProtocol +from sqlspec.storage.backends.local import LocalStore from sqlspec.storage.errors import execute_sync_storage_operation from sqlspec.storage.registry import storage_registry from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED, PYARROW_INSTALLED +from sqlspec.utils.module_loader import import_optional, import_optional_attr from tests.fixtures.rustfs import rustfs_fsspec_kwargs, rustfs_obstore_kwargs -# Test data -TEST_TEXT_CONTENT = "Hello, SQLSpec storage integration test!" -TEST_BINARY_CONTENT = ( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde" -) +if TYPE_CHECKING: + from pathlib import Path + from pytest_databases.docker.rustfs import RustfsService -def _find_log_record(records: "list[logging.LogRecord]", message: str, logger_name: str) -> "logging.LogRecord": - for record in records: - if record.name != logger_name: - continue - if record.getMessage() == message: - return record - msg = f"Expected log message '{message}' from '{logger_name}' not found" - raise AssertionError(msg) + from sqlspec.protocols import ObjectStoreProtocol + from sqlspec.storage.backends.fsspec import FSSpecBackend + from sqlspec.storage.backends.obstore import ObStoreBackend +else: + FSSpecBackend = import_optional_attr("sqlspec.storage.backends.fsspec", "FSSpecBackend") + ObStoreBackend = import_optional_attr("sqlspec.storage.backends.obstore", "ObStoreBackend") +pyarrow: Any = import_optional("pyarrow") -def _raise_missing() -> None: - raise FileNotFoundError("missing") +TEST_TEXT_CONTENT = "Hello, SQLSpec storage integration test!" +TEST_BINARY_CONTENT = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde" +) @pytest.fixture @@ -45,11 +40,9 @@ def local_test_setup(tmp_path: "Path") -> "Path": test_dir = tmp_path / "storage_test" test_dir.mkdir() - # Create sample files (test_dir / "test.txt").write_text(TEST_TEXT_CONTENT) (test_dir / "test.bin").write_bytes(TEST_BINARY_CONTENT) - # Create subdirectory structure subdir = test_dir / "subdir" subdir.mkdir() (subdir / "nested.txt").write_text("Nested file content") @@ -60,34 +53,52 @@ def local_test_setup(tmp_path: "Path") -> "Path": @pytest.fixture def fsspec_s3_backend(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": """Set up FSSpec S3 backend for testing.""" - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") return FSSpecBackend(uri=f"s3://{rustfs_bucket_name}/", **rustfs_fsspec_kwargs(rustfs_service)) @pytest.fixture def obstore_s3_backend(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": """Set up ObStore S3 backend for testing.""" - from sqlspec.storage.backends.obstore import ObStoreBackend - s3_uri = f"s3://{rustfs_bucket_name}" return ObStoreBackend(s3_uri, **rustfs_obstore_kwargs(rustfs_service)) -# Local storage tests +@pytest.fixture +def local_backend(tmp_path: "Path") -> "ObjectStoreProtocol": + """Create LocalStore backend.""" + return LocalStore(str(tmp_path)) -def test_local_store_file_operations(local_test_setup: Path) -> None: - """Test LocalStore basic file operations.""" - from sqlspec.storage.backends.local import LocalStore +@pytest.fixture +def fsspec_s3_backend_optional(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": + """Create FSSpec S3 backend if available.""" + if not FSSPEC_INSTALLED: + pytest.skip("fsspec missing") + pytest.importorskip("s3fs") + return FSSpecBackend.from_config({ + "protocol": "s3", + "fs_config": rustfs_fsspec_kwargs(rustfs_service), + "base_path": rustfs_bucket_name, + }) + + +@pytest.fixture +def obstore_s3_backend_optional(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": + """Create ObStore S3 backend if available.""" + if not OBSTORE_INSTALLED: + pytest.skip("obstore missing") + s3_uri = f"s3://{rustfs_bucket_name}" + return ObStoreBackend(s3_uri, **rustfs_obstore_kwargs(rustfs_service)) + +def test_local_store_file_operations(local_test_setup: "Path") -> None: + """Test LocalStore basic file operations.""" store = LocalStore(str(local_test_setup)) - # Test exists assert store.exists_sync("test.txt") assert not store.exists_sync("nonexistent.txt") - # Test read operations text_content = store.read_text_sync("test.txt") assert text_content == TEST_TEXT_CONTENT @@ -95,73 +106,59 @@ def test_local_store_file_operations(local_test_setup: Path) -> None: assert binary_content == TEST_BINARY_CONTENT -def test_local_store_write_operations(local_test_setup: Path) -> None: +def test_local_store_write_operations(local_test_setup: "Path") -> None: """Test LocalStore write operations.""" - from sqlspec.storage.backends.local import LocalStore - store = LocalStore(str(local_test_setup)) - # Test write text new_text = "New text content" store.write_text_sync("new.txt", new_text) assert store.read_text_sync("new.txt") == new_text - # Test write bytes new_bytes = b"New binary content" store.write_bytes_sync("new.bin", new_bytes) assert store.read_bytes_sync("new.bin") == new_bytes -def test_local_store_listing_operations(local_test_setup: Path) -> None: +def test_local_store_listing_operations(local_test_setup: "Path") -> None: """Test LocalStore listing operations.""" - from sqlspec.storage.backends.local import LocalStore - store = LocalStore(str(local_test_setup)) - # Test list_objects objects = store.list_objects_sync() assert "test.txt" in objects assert "test.bin" in objects assert "subdir/nested.txt" in objects -def test_local_store_url_signing_not_supported(local_test_setup: Path) -> None: +def test_local_store_url_signing_not_supported(local_test_setup: "Path") -> None: """Test LocalStore URL signing raises NotImplementedError.""" - from sqlspec.storage.backends.local import LocalStore - store = LocalStore(str(local_test_setup)) - # Local storage does not support URL signing assert store.supports_signing is False with pytest.raises(NotImplementedError, match="URL signing is not applicable"): store.sign_sync("test.txt", expires_in=3600) -async def test_local_store_async_operations(local_test_setup: Path) -> None: +async def test_local_store_async_operations(local_test_setup: "Path") -> None: """Test LocalStore async operations.""" - from sqlspec.storage.backends.local import LocalStore - store = LocalStore(str(local_test_setup)) - # Test async exists exists = await store.exists_async("test.txt") assert exists - # Test async read operations text_content = await store.read_text_async("test.txt") assert text_content == TEST_TEXT_CONTENT binary_content = await store.read_bytes_async("test.bin") assert binary_content == TEST_BINARY_CONTENT - # Test async write operations new_text = "Async new text content" await store.write_text_async("async_new.txt", new_text) assert await store.read_text_async("async_new.txt") == new_text -def test_storage_missing_logging_format(caplog) -> None: +def test_storage_missing_logging_format(caplog: pytest.LogCaptureFixture) -> None: + """Test structured logging fields emitted when a storage object is missing.""" caplog.set_level(logging.INFO, logger="sqlspec.storage.errors") with pytest.raises(FileNotFoundInStorageError): @@ -177,24 +174,21 @@ def test_storage_missing_logging_format(caplog) -> None: assert extra_fields.get("retryable") is False -# FSSpec S3 backend tests - - +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_basic_operations(fsspec_s3_backend: "ObjectStoreProtocol") -> None: """Test FSSpec S3 backend basic operations.""" - # Test write and read text test_path = "integration_test/test.txt" fsspec_s3_backend.write_text_sync(test_path, TEST_TEXT_CONTENT) content = fsspec_s3_backend.read_text_sync(test_path) assert content == TEST_TEXT_CONTENT - # Test exists assert fsspec_s3_backend.exists_sync(test_path) assert not fsspec_s3_backend.exists_sync("nonexistent.txt") +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_binary_operations(fsspec_s3_backend: "ObjectStoreProtocol") -> None: """Test FSSpec S3 backend binary operations.""" @@ -205,30 +199,28 @@ def test_fsspec_s3_binary_operations(fsspec_s3_backend: "ObjectStoreProtocol") - assert content == TEST_BINARY_CONTENT +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") async def test_fsspec_s3_async_operations(fsspec_s3_backend: "ObjectStoreProtocol") -> None: """Test FSSpec S3 backend async operations.""" test_path = "integration_test/async_test.txt" - # Test async operations await fsspec_s3_backend.write_text_async(test_path, TEST_TEXT_CONTENT) content = await fsspec_s3_backend.read_text_async(test_path) assert content == TEST_TEXT_CONTENT - # Test async exists exists = await fsspec_s3_backend.exists_async(test_path) assert exists +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_listing_operations(fsspec_s3_backend: "ObjectStoreProtocol") -> None: """Test FSSpec S3 backend listing operations.""" - # Write multiple test files test_files = ["list_test/file1.txt", "list_test/file2.txt", "list_test/subdir/file3.txt"] for file_path in test_files: fsspec_s3_backend.write_text_sync(file_path, f"Content of {file_path}") - # Test list_objects objects = fsspec_s3_backend.list_objects_sync("list_test/") assert len(objects) >= 3 assert any("file1.txt" in obj for obj in objects) @@ -236,10 +228,10 @@ def test_fsspec_s3_listing_operations(fsspec_s3_backend: "ObjectStoreProtocol") assert any("file3.txt" in obj for obj in objects) +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_copy_move_operations(fsspec_s3_backend: "ObjectStoreProtocol") -> None: """Test FSSpec S3 backend copy and move operations.""" - # Setup source file source_path = "copy_test/source.txt" copy_path = "copy_test/copy.txt" move_source_path = "move_test/source.txt" @@ -248,36 +240,31 @@ def test_fsspec_s3_copy_move_operations(fsspec_s3_backend: "ObjectStoreProtocol" fsspec_s3_backend.write_text_sync(source_path, TEST_TEXT_CONTENT) fsspec_s3_backend.write_text_sync(move_source_path, TEST_TEXT_CONTENT) - # Test copy fsspec_s3_backend.copy_sync(source_path, copy_path) - assert fsspec_s3_backend.exists_sync(source_path) # Original should still exist + assert fsspec_s3_backend.exists_sync(source_path) assert fsspec_s3_backend.exists_sync(copy_path) assert fsspec_s3_backend.read_text_sync(copy_path) == TEST_TEXT_CONTENT - # Test move fsspec_s3_backend.move_sync(move_source_path, move_dest_path) - assert not fsspec_s3_backend.exists_sync(move_source_path) # Original should be gone + assert not fsspec_s3_backend.exists_sync(move_source_path) assert fsspec_s3_backend.exists_sync(move_dest_path) assert fsspec_s3_backend.read_text_sync(move_dest_path) == TEST_TEXT_CONTENT -# ObStore S3 backend tests - - +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not OBSTORE_INSTALLED, reason="obstore missing") def test_obstore_s3_basic_operations(obstore_s3_backend: "ObjectStoreProtocol") -> None: """Test ObStore S3 backend basic operations.""" test_path = "integration_test/obstore_test.txt" - # Test write and read obstore_s3_backend.write_text_sync(test_path, TEST_TEXT_CONTENT) content = obstore_s3_backend.read_text_sync(test_path) assert content == TEST_TEXT_CONTENT - # Test exists assert obstore_s3_backend.exists_sync(test_path) +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not OBSTORE_INSTALLED, reason="obstore missing") def test_obstore_s3_binary_operations(obstore_s3_backend: "ObjectStoreProtocol") -> None: """Test ObStore S3 backend binary operations.""" @@ -288,12 +275,12 @@ def test_obstore_s3_binary_operations(obstore_s3_backend: "ObjectStoreProtocol") assert content == TEST_BINARY_CONTENT +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not OBSTORE_INSTALLED, reason="obstore missing") async def test_obstore_s3_async_operations(obstore_s3_backend: "ObjectStoreProtocol") -> None: """Test ObStore S3 backend async operations.""" test_path = "integration_test/obstore_async.txt" - # Test async operations await obstore_s3_backend.write_text_async(test_path, TEST_TEXT_CONTENT) content = await obstore_s3_backend.read_text_async(test_path) assert content == TEST_TEXT_CONTENT @@ -302,38 +289,29 @@ async def test_obstore_s3_async_operations(obstore_s3_backend: "ObjectStoreProto assert exists +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not OBSTORE_INSTALLED, reason="obstore missing") def test_obstore_s3_listing_operations(obstore_s3_backend: "ObjectStoreProtocol") -> None: """Test ObStore S3 backend listing operations.""" - # Write test files in different paths test_files = ["obstore_list/file1.txt", "obstore_list/file2.txt", "obstore_list/subdir/file3.txt"] for file_path in test_files: obstore_s3_backend.write_text_sync(file_path, f"ObStore content of {file_path}") - # Test list_objects objects = obstore_s3_backend.list_objects_sync("obstore_list/") assert len(objects) >= 3 assert any("file1.txt" in obj for obj in objects) assert any("file2.txt" in obj for obj in objects) -# Storage registry tests - - def test_registry_uri_resolution_local(tmp_path: "Path") -> None: """Test storage registry URI resolution for local files.""" - from sqlspec.storage.backends.local import LocalStore - from sqlspec.storage.backends.obstore import ObStoreBackend - - # Test file URI resolution test_file = tmp_path / "registry_test.txt" test_file.write_text(TEST_TEXT_CONTENT) - # Test file:// URI file_uri = f"file://{test_file}" backend = storage_registry.get(file_uri) - # Registry prefers obstore for file:// URIs when available, otherwise LocalStore - assert isinstance(backend, (ObStoreBackend, LocalStore)) + expected_types = (ObStoreBackend, LocalStore) if OBSTORE_INSTALLED else (LocalStore,) + assert isinstance(backend, expected_types) content = backend.read_text_sync("registry_test.txt") assert content == TEST_TEXT_CONTENT @@ -341,68 +319,53 @@ def test_registry_uri_resolution_local(tmp_path: "Path") -> None: def test_registry_path_resolution(tmp_path: "Path") -> None: """Test storage registry resolution for raw paths.""" - from sqlspec.storage.backends.local import LocalStore - from sqlspec.storage.backends.obstore import ObStoreBackend - - # Test Path object resolution test_file = tmp_path / "path_test.txt" test_file.write_text(TEST_TEXT_CONTENT) backend = storage_registry.get(tmp_path) - # Registry prefers obstore for local paths when available, otherwise LocalStore - assert isinstance(backend, (ObStoreBackend, LocalStore)) + expected_types = (ObStoreBackend, LocalStore) if OBSTORE_INSTALLED else (LocalStore,) + assert isinstance(backend, expected_types) content = backend.read_text_sync("path_test.txt") assert content == TEST_TEXT_CONTENT +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_registry_s3_fsspec_resolution(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: """Test storage registry S3 resolution with FSSpec backend.""" - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") s3_uri = f"s3://{rustfs_bucket_name}/registry_test/" backend = storage_registry.get(s3_uri, backend="fsspec", **rustfs_fsspec_kwargs(rustfs_service)) - - # Should get FSSpec backend for S3 assert isinstance(backend, FSSpecBackend) - # Test basic operations test_path = "registry_fsspec_test.txt" backend.write_text_sync(test_path, TEST_TEXT_CONTENT) content = backend.read_text_sync(test_path) assert content == TEST_TEXT_CONTENT +@pytest.mark.xdist_group("rustfs") def test_registry_alias_registration( rustfs_service: "RustfsService", rustfs_bucket_name: str, tmp_path: "Path" ) -> None: """Test storage registry alias registration and usage.""" - from sqlspec.storage.backends.local import LocalStore - from sqlspec.storage.backends.obstore import ObStoreBackend - - # Clear registry to avoid test interference storage_registry.clear() try: - # Register local alias storage_registry.register_alias("test-local", uri=f"file://{tmp_path / 'test_data'}") - # Test local alias backend = storage_registry.get("test-local") - # Registry prefers obstore for local paths when available, otherwise LocalStore - assert isinstance(backend, (ObStoreBackend, LocalStore)) + expected_types = (ObStoreBackend, LocalStore) if OBSTORE_INSTALLED else (LocalStore,) + assert isinstance(backend, expected_types) - # Create test data backend.write_text_sync("alias_test.txt", TEST_TEXT_CONTENT) content = backend.read_text_sync("alias_test.txt") assert content == TEST_TEXT_CONTENT - # Register S3 alias if fsspec available if FSSPEC_INSTALLED: - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") storage_registry.register_alias( "test-s3", uri=f"s3://{rustfs_bucket_name}/", backend="fsspec", **rustfs_fsspec_kwargs(rustfs_service) ) @@ -410,54 +373,15 @@ def test_registry_alias_registration( s3_backend = storage_registry.get("test-s3") assert isinstance(s3_backend, FSSpecBackend) - # Test S3 alias operations s3_backend.write_text_sync("s3_alias_test.txt", TEST_TEXT_CONTENT) s3_content = s3_backend.read_text_sync("s3_alias_test.txt") assert s3_content == TEST_TEXT_CONTENT finally: - # Clean up registry storage_registry.clear() -# Backend comparison tests - - -@pytest.fixture -def local_backend(tmp_path: "Path") -> "ObjectStoreProtocol": - """Create LocalStore backend.""" - from sqlspec.storage.backends.local import LocalStore - - return LocalStore(str(tmp_path)) - - -@pytest.fixture -def fsspec_s3_backend_optional(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": - """Create FSSpec S3 backend if available.""" - if not FSSPEC_INSTALLED: - pytest.skip("fsspec missing") - - from sqlspec.storage.backends.fsspec import FSSpecBackend - - return FSSpecBackend.from_config({ - "protocol": "s3", - "fs_config": rustfs_fsspec_kwargs(rustfs_service), - "base_path": rustfs_bucket_name, - }) - - -@pytest.fixture -def obstore_s3_backend_optional(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> "ObjectStoreProtocol": - """Create ObStore S3 backend if available.""" - if not OBSTORE_INSTALLED: - pytest.skip("obstore missing") - - from sqlspec.storage.backends.obstore import ObStoreBackend - - s3_uri = f"s3://{rustfs_bucket_name}" - return ObStoreBackend(s3_uri, **rustfs_obstore_kwargs(rustfs_service)) - - +@pytest.mark.xdist_group("rustfs") @pytest.mark.parametrize("backend_name", ["local_backend", "fsspec_s3_backend_optional", "obstore_s3_backend_optional"]) def test_backend_consistency(request: pytest.FixtureRequest, backend_name: str) -> None: """Test that all backends provide consistent behavior.""" @@ -467,15 +391,12 @@ def test_backend_consistency(request: pytest.FixtureRequest, backend_name: str) test_path = f"consistency_test_{backend_name}.txt" - # Test write/read consistency backend.write_text_sync(test_path, TEST_TEXT_CONTENT) content = backend.read_text_sync(test_path) assert content == TEST_TEXT_CONTENT - # Test exists consistency assert backend.exists_sync(test_path) - # Test URL signing consistency (only for backends that support signing) if backend.supports_signing: signed_url = backend.sign_sync(test_path, expires_in=3600) assert isinstance(signed_url, str) @@ -485,6 +406,7 @@ def test_backend_consistency(request: pytest.FixtureRequest, backend_name: str) backend.sign_sync(test_path, expires_in=3600) +@pytest.mark.xdist_group("rustfs") def test_s3_backend_resolved_uri_consistency( fsspec_s3_backend_optional: "ObjectStoreProtocol", obstore_s3_backend_optional: "ObjectStoreProtocol", @@ -502,6 +424,7 @@ def test_s3_backend_resolved_uri_consistency( assert obstore_s3_backend_optional.resolve_uri(test_path) == expected +@pytest.mark.xdist_group("rustfs") @pytest.mark.parametrize("backend_name", ["local_backend", "fsspec_s3_backend_optional", "obstore_s3_backend_optional"]) async def test_backend_async_consistency(request: pytest.FixtureRequest, backend_name: str) -> None: """Test that all backends provide consistent async behavior.""" @@ -511,26 +434,18 @@ async def test_backend_async_consistency(request: pytest.FixtureRequest, backend test_path = f"async_consistency_{backend_name}.txt" - # Test async write/read consistency await backend.write_text_async(test_path, TEST_TEXT_CONTENT) content = await backend.read_text_async(test_path) assert content == TEST_TEXT_CONTENT - # Test async exists consistency exists = await backend.exists_async(test_path) assert exists -# Error handling tests - - def test_local_backend_error_handling(tmp_path: "Path") -> None: """Test LocalStore error handling for invalid operations.""" - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) - # Test reading nonexistent file with pytest.raises(FileNotFoundError): backend.read_text_sync("nonexistent.txt") @@ -538,29 +453,25 @@ def test_local_backend_error_handling(tmp_path: "Path") -> None: backend.read_bytes_sync("nonexistent.txt") +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_error_handling(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: """Test FSSpec S3 backend error handling.""" - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") backend = FSSpecBackend.from_config({ "protocol": "s3", "fs_config": rustfs_fsspec_kwargs(rustfs_service), "base_path": rustfs_bucket_name, }) - # Test reading nonexistent file with pytest.raises(FileNotFoundInStorageError): backend.read_text_sync("nonexistent.txt") async def test_async_error_handling(tmp_path: "Path") -> None: """Test async error handling.""" - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) - # Test async reading nonexistent file with pytest.raises(FileNotFoundError): await backend.read_text_async("nonexistent.txt") @@ -568,9 +479,6 @@ async def test_async_error_handling(tmp_path: "Path") -> None: await backend.read_bytes_async("nonexistent.txt") -# Registry advanced tests - - def test_registry_caching_behavior(tmp_path: "Path") -> None: """Test that storage registry properly caches backend instances.""" storage_registry.clear() @@ -578,18 +486,14 @@ def test_registry_caching_behavior(tmp_path: "Path") -> None: try: uri = f"file://{tmp_path}" - # Get same URI twice backend1 = storage_registry.get(uri) backend2 = storage_registry.get(uri) - # Should return the same instance (cached) assert backend1 is backend2 - # Clear cache and get again storage_registry.clear_cache(uri) backend3 = storage_registry.get(uri) - # Should be different instance after cache clear assert backend1 is not backend3 finally: @@ -601,19 +505,15 @@ def test_registry_alias_management(tmp_path: "Path") -> None: storage_registry.clear() try: - # Register alias alias_name = "test-management" storage_registry.register_alias(alias_name, uri=f"file://{tmp_path}") - # Test alias registration check assert storage_registry.is_alias_registered(alias_name) assert not storage_registry.is_alias_registered("nonexistent-alias") - # Test list aliases aliases = storage_registry.list_aliases() assert alias_name in aliases - # Test clearing aliases storage_registry.clear_aliases() assert not storage_registry.is_alias_registered(alias_name) @@ -621,97 +521,71 @@ def test_registry_alias_management(tmp_path: "Path") -> None: storage_registry.clear() +@pytest.mark.xdist_group("rustfs") def test_registry_backend_fallback_order( tmp_path: "Path", rustfs_service: "RustfsService", rustfs_bucket_name: str ) -> None: """Test that registry follows correct backend fallback order.""" - from sqlspec.storage.backends.local import LocalStore - from sqlspec.storage.backends.obstore import ObStoreBackend - storage_registry.clear() try: - # Test local file resolution (prefers ObStore > FSSpec > LocalStore) local_uri = f"file://{tmp_path}" local_backend = storage_registry.get(local_uri) - # Should get ObStore if available, else FSSpec, else LocalStore - assert isinstance(local_backend, (ObStoreBackend, LocalStore)) + expected_types = (ObStoreBackend, LocalStore) if OBSTORE_INSTALLED else (LocalStore,) + assert isinstance(local_backend, expected_types) - # Test S3 resolution (should prefer ObStore > FSSpec if available) s3_uri = f"s3://{rustfs_bucket_name}" storage_kwargs = ( rustfs_obstore_kwargs(rustfs_service) if OBSTORE_INSTALLED else rustfs_fsspec_kwargs(rustfs_service) ) s3_backend = storage_registry.get(s3_uri, **storage_kwargs) - # Should get ObStore if available, else FSSpec, else error if OBSTORE_INSTALLED: - from sqlspec.storage.backends.obstore import ObStoreBackend - assert isinstance(s3_backend, ObStoreBackend) elif FSSPEC_INSTALLED: - from sqlspec.storage.backends.fsspec import FSSpecBackend - assert isinstance(s3_backend, FSSpecBackend) - else: - # Should raise MissingDependencyError if no cloud backends available - pass finally: storage_registry.clear() -# Arrow integration tests - - @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow missing") def test_local_arrow_operations(tmp_path: "Path") -> None: """Test LocalStore Arrow operations if pyarrow is available.""" - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) - # Create test Arrow data - import pyarrow as pa - data: dict[str, Any] = {"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"], "col3": [1.1, 2.2, 3.3, 4.4]} - table = pa.table(data) + table = pyarrow.table(data) - # Test write/read Arrow table arrow_path = "arrow_test.parquet" backend.write_arrow_sync(arrow_path, table) read_table = backend.read_arrow_sync(arrow_path) assert read_table.equals(table) - # Test exists for Arrow file assert backend.exists_sync(arrow_path) +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow missing") def test_fsspec_s3_arrow_operations(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: """Test FSSpec S3 backend Arrow operations if pyarrow is available.""" - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") backend = FSSpecBackend.from_config({ "protocol": "s3", "fs_config": rustfs_fsspec_kwargs(rustfs_service), "base_path": rustfs_bucket_name, }) - import pyarrow as pa - - # Create test data with different types data: dict[str, Any] = { "integers": [1, 2, 3, 4, 5], "strings": ["hello", "world", "storage", "test", "arrow"], "floats": [1.1, 2.2, 3.3, 4.4, 5.5], "booleans": [True, False, True, False, True], } - table = pa.table(data) + table = pyarrow.table(data) - # Test S3 Arrow operations s3_arrow_path = "s3_arrow_test.parquet" backend.write_arrow_sync(s3_arrow_path, table) @@ -719,27 +593,19 @@ def test_fsspec_s3_arrow_operations(rustfs_service: "RustfsService", rustfs_buck assert read_table.equals(table) -# Performance tests - - def test_local_backend_large_file_operations(tmp_path: "Path") -> None: """Test LocalStore with larger file operations.""" - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) - # Create larger test content large_text = "Large file content line\n" * 1000 large_binary = b"Binary data chunk" * 1000 - # Test large text operations large_text_path = "large_test.txt" backend.write_text_sync(large_text_path, large_text) read_content = backend.read_text_sync(large_text_path) assert read_content == large_text assert len(read_content) == len(large_text) - # Test large binary operations large_binary_path = "large_test.bin" backend.write_bytes_sync(large_binary_path, large_binary) read_binary = backend.read_bytes_sync(large_binary_path) @@ -749,10 +615,6 @@ def test_local_backend_large_file_operations(tmp_path: "Path") -> None: async def test_concurrent_storage_operations(tmp_path: "Path") -> None: """Test concurrent storage operations.""" - import asyncio - - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) async def write_test_file(index: int) -> None: @@ -761,39 +623,28 @@ async def write_test_file(index: int) -> None: content = f"Concurrent test content {index}" await backend.write_text_async(path, content) - # Verify the write read_content = await backend.read_text_async(path) assert read_content == content - # Run multiple concurrent writes tasks = [write_test_file(i) for i in range(10)] await asyncio.gather(*tasks) - # Verify all files exist for i in range(10): assert backend.exists_sync(f"concurrent_test_{i}.txt") -# Metadata tests - - def test_local_metadata_operations(tmp_path: "Path") -> None: """Test LocalStore metadata retrieval.""" - from sqlspec.storage.backends.local import LocalStore - backend = LocalStore(str(tmp_path)) - # Create test file test_path = "metadata_test.txt" backend.write_text_sync(test_path, TEST_TEXT_CONTENT) - # Test metadata retrieval metadata = backend.get_metadata_sync(test_path) assert metadata is not None assert "size" in metadata assert metadata["size"] == len(TEST_TEXT_CONTENT.encode()) - # Test metadata for binary file binary_path = "metadata_binary.bin" backend.write_bytes_sync(binary_path, TEST_BINARY_CONTENT) @@ -802,21 +653,104 @@ def test_local_metadata_operations(tmp_path: "Path") -> None: assert binary_metadata["size"] == len(TEST_BINARY_CONTENT) +@pytest.mark.xdist_group("rustfs") @pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") def test_fsspec_s3_metadata_operations(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: """Test FSSpec S3 backend metadata operations.""" - from sqlspec.storage.backends.fsspec import FSSpecBackend - + pytest.importorskip("s3fs") backend = FSSpecBackend.from_config({ "protocol": "s3", "fs_config": rustfs_fsspec_kwargs(rustfs_service), "base_path": rustfs_bucket_name, }) - # Test S3 metadata test_path = "s3_metadata_test.txt" backend.write_text_sync(test_path, TEST_TEXT_CONTENT) metadata = backend.get_metadata_sync(test_path) assert metadata is not None assert "size" in metadata + + +@pytest.mark.xdist_group("floci_gcp") +async def test_floci_gcs_obstore_operations(floci_gcs_obstore: "ObjectStoreProtocol") -> None: + """Verify sync/async object lifecycle and Arrow operations on Floci GCS via ObStoreBackend.""" + await _assert_cloud_backend_operations(floci_gcs_obstore, "floci_gcs_obstore") + + +@pytest.mark.xdist_group("floci_gcp") +async def test_floci_gcs_fsspec_operations(floci_gcs_fsspec: "ObjectStoreProtocol") -> None: + """Verify sync/async object lifecycle and Arrow operations on Floci GCS via FSSpecBackend.""" + await _assert_cloud_backend_operations(floci_gcs_fsspec, "floci_gcs_fsspec") + + +@pytest.mark.xdist_group("floci_aws") +async def test_floci_aws_s3_obstore_operations(floci_aws_s3_obstore: "ObjectStoreProtocol") -> None: + """Verify sync/async object lifecycle and Arrow operations on Floci AWS S3 via ObStoreBackend.""" + await _assert_cloud_backend_operations(floci_aws_s3_obstore, "floci_aws_s3_obstore") + + +@pytest.mark.xdist_group("azure_blob") +async def test_azurite_obstore_operations(azurite_obstore: "ObjectStoreProtocol") -> None: + """Verify sync/async object lifecycle and Arrow operations on Azurite via ObStoreBackend.""" + await _assert_cloud_backend_operations(azurite_obstore, "azurite_obstore") + + +@pytest.mark.xdist_group("azure_blob") +async def test_azurite_fsspec_operations(azurite_fsspec: "ObjectStoreProtocol") -> None: + """Verify sync/async object lifecycle and Arrow operations on Azurite via FSSpecBackend.""" + await _assert_cloud_backend_operations(azurite_fsspec, "azurite_fsspec") + + +async def _assert_cloud_backend_operations(backend: "ObjectStoreProtocol", prefix: str) -> None: + """Verify sync/async bytes, existence, listing, copy, delete, and Arrow Parquet round-trips.""" + sync_path = f"{prefix}/sync.bin" + async_path = f"{prefix}/async.bin" + copy_path = f"{prefix}/copied.bin" + + backend.write_bytes_sync(sync_path, TEST_BINARY_CONTENT) + assert backend.exists_sync(sync_path) is True + assert backend.read_bytes_sync(sync_path) == TEST_BINARY_CONTENT + + await backend.write_bytes_async(async_path, TEST_TEXT_CONTENT.encode("utf-8")) + assert await backend.exists_async(async_path) is True + assert await backend.read_bytes_async(async_path) == TEST_TEXT_CONTENT.encode("utf-8") + + backend.copy_sync(sync_path, copy_path) + assert backend.exists_sync(copy_path) is True + assert backend.read_bytes_sync(copy_path) == TEST_BINARY_CONTENT + + listed = backend.list_objects_sync(f"{prefix}/") + assert any("sync.bin" in entry for entry in listed) + assert any("async.bin" in entry for entry in listed) + assert any("copied.bin" in entry for entry in listed) + + backend.delete_sync(copy_path) + assert backend.exists_sync(copy_path) is False + + if PYARROW_INSTALLED and pyarrow is not None: + arrow_path = f"{prefix}/table.parquet" + expected_table = pyarrow.table({ + "id": [10, 20, 30], + "label": ["gcs", "s3", "azure"], + "active": [True, False, True], + }) + backend.write_arrow_sync(arrow_path, expected_table) + loaded_table = backend.read_arrow_sync(arrow_path) + assert loaded_table.equals(expected_table) + + +def _find_log_record(records: "list[logging.LogRecord]", message: str, logger_name: str) -> "logging.LogRecord": + """Locate a log record matching exact message and logger name.""" + for record in records: + if record.name != logger_name: + continue + if record.getMessage() == message: + return record + msg = f"Expected log message '{message}' from '{logger_name}' not found" + raise AssertionError(msg) + + +def _raise_missing() -> None: + """Raise FileNotFoundError to test storage error logging translation.""" + raise FileNotFoundError("missing") diff --git a/tests/integration/storage/test_streaming.py b/tests/integration/storage/test_streaming.py index d501a701a..f3f4f6c05 100644 --- a/tests/integration/storage/test_streaming.py +++ b/tests/integration/storage/test_streaming.py @@ -1,47 +1,204 @@ +"""Integration tests for sync and async storage streaming across local and multi-cloud backends.""" + +from typing import TYPE_CHECKING, Any + import pytest from sqlspec.storage.pipeline import AsyncStoragePipeline, SyncStoragePipeline +from sqlspec.storage.registry import StorageRegistry +from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED, PYARROW_INSTALLED +from sqlspec.utils.module_loader import import_optional, import_optional_attr +from tests.fixtures.rustfs import rustfs_fsspec_kwargs, rustfs_obstore_kwargs +from tests.integration.fixtures.cloud_storage import floci_gcs_obstore_kwargs + +if TYPE_CHECKING: + from pathlib import Path + + from pytest_databases.docker.azure_blob import AzureBlobService + from pytest_databases.docker.floci import FlociGCPService + from pytest_databases.docker.rustfs import RustfsService + + from sqlspec.protocols import ObjectStoreProtocol + from sqlspec.storage.backends.fsspec import FSSpecBackend + from sqlspec.storage.backends.obstore import ObStoreBackend +else: + FSSpecBackend = import_optional_attr("sqlspec.storage.backends.fsspec", "FSSpecBackend") + ObStoreBackend = import_optional_attr("sqlspec.storage.backends.obstore", "ObStoreBackend") + +pyarrow: Any = import_optional("pyarrow") + +STREAM_CHUNK_SIZE = 1024 +STREAM_PAYLOAD = b"sqlspec-streaming-chunk-" * 4000 @pytest.fixture -def test_file(tmp_path): - f = tmp_path / "test.txt" - f.write_text("hello world" * 1000) - return f +def test_file(tmp_path: "Path") -> "Path": + """Create a multi-kilobyte local file for chunked streaming verification.""" + target = tmp_path / "test.txt" + target.write_bytes(STREAM_PAYLOAD) + return target -def test_sync_stream_read_local(test_file): +def test_sync_stream_read_local(test_file: "Path") -> None: + """Verify SyncStoragePipeline streams local file bytes across multiple chunks.""" pipeline = SyncStoragePipeline() - stream = pipeline.stream_read(test_file, chunk_size=10) - content = b"".join(stream) - assert content == b"hello world" * 1000 + chunks = list(pipeline.stream_read(test_file, chunk_size=STREAM_CHUNK_SIZE)) + assert len(chunks) > 1 + assert b"".join(chunks) == STREAM_PAYLOAD -async def test_async_stream_read_local(test_file): +async def test_async_stream_read_local(test_file: "Path") -> None: + """Verify AsyncStoragePipeline streams local file bytes across multiple chunks.""" pipeline = AsyncStoragePipeline() - stream = await pipeline.stream_read_async(test_file, chunk_size=10) - content = b"" - async for chunk in stream: - content += chunk - assert content == b"hello world" * 1000 + stream = await pipeline.stream_read_async(test_file, chunk_size=STREAM_CHUNK_SIZE) + chunks = [chunk async for chunk in stream] + assert len(chunks) > 1 + assert b"".join(chunks) == STREAM_PAYLOAD -async def test_async_stream_read_local_explicit_uri(test_file): +async def test_async_stream_read_local_explicit_uri(test_file: "Path") -> None: + """Verify AsyncStoragePipeline streams file:// URIs across multiple chunks.""" pipeline = AsyncStoragePipeline() - uri = f"file://{test_file}" - stream = await pipeline.stream_read_async(uri, chunk_size=10) - content = b"" - async for chunk in stream: - content += chunk - assert content == b"hello world" * 1000 - - -def test_sync_stream_read_fsspec(test_file): - # Using explicit fsspec backend via protocol - SyncStoragePipeline() - # Force fsspec if possible, or just rely on registry resolution which defaults to LocalStore for file:// - # To test FSSpecBackend specifically, we can instantiate it or use a different protocol if we had one mocked. - # But we can try to force it if we had a config. - # Actually, for file:// it picks LocalStore if not configured otherwise. - # Let's trust FSSpecBackend implementation matches LocalStore pattern which is verified. - pass + stream = await pipeline.stream_read_async(f"file://{test_file}", chunk_size=STREAM_CHUNK_SIZE) + chunks = [chunk async for chunk in stream] + assert len(chunks) > 1 + assert b"".join(chunks) == STREAM_PAYLOAD + + +@pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") +def test_sync_stream_read_fsspec(test_file: "Path") -> None: + """Verify FSSpecBackend and SyncStoragePipeline stream local files across multiple chunks.""" + backend = FSSpecBackend(f"file://{test_file.parent}") + direct_chunks = list(backend.stream_read_sync(test_file.name, chunk_size=STREAM_CHUNK_SIZE)) + assert len(direct_chunks) > 1 + assert b"".join(direct_chunks) == STREAM_PAYLOAD + + registry = StorageRegistry() + registry.register_alias("local-fsspec-stream", f"file://{test_file.parent}", backend="fsspec") + pipeline = SyncStoragePipeline(registry=registry) + pipeline_chunks = list( + pipeline.stream_read(f"alias://local-fsspec-stream/{test_file.name}", chunk_size=STREAM_CHUNK_SIZE) + ) + assert len(pipeline_chunks) > 1 + assert b"".join(pipeline_chunks) == STREAM_PAYLOAD + + +@pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") +async def test_async_stream_read_fsspec(test_file: "Path") -> None: + """Verify FSSpecBackend async chunked streaming reassembles payloads accurately.""" + backend = FSSpecBackend(f"file://{test_file.parent}") + stream = await backend.stream_read_async(test_file.name, chunk_size=STREAM_CHUNK_SIZE) + chunks = [chunk async for chunk in stream] + assert len(chunks) > 1 + assert b"".join(chunks) == STREAM_PAYLOAD + + +@pytest.mark.xdist_group("rustfs") +@pytest.mark.skipif(not FSSPEC_INSTALLED, reason="fsspec missing") +async def test_rustfs_s3_fsspec_streaming(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: + """Verify sync and async chunked streaming against RustFS S3 via FSSpecBackend.""" + pytest.importorskip("s3fs") + backend = FSSpecBackend(f"s3://{rustfs_bucket_name}", **rustfs_fsspec_kwargs(rustfs_service)) + await _assert_backend_streaming_and_arrow(backend, "streaming/rustfs_fsspec") + + +@pytest.mark.xdist_group("rustfs") +@pytest.mark.skipif(not OBSTORE_INSTALLED, reason="obstore missing") +async def test_rustfs_s3_obstore_streaming(rustfs_service: "RustfsService", rustfs_bucket_name: str) -> None: + """Verify sync and async chunked streaming against RustFS S3 via ObStoreBackend.""" + backend = ObStoreBackend(f"s3://{rustfs_bucket_name}", **rustfs_obstore_kwargs(rustfs_service)) + await _assert_backend_streaming_and_arrow(backend, "streaming/rustfs_obstore") + + +@pytest.mark.xdist_group("floci_gcp") +async def test_floci_gcs_obstore_streaming_and_pipeline( + floci_gcp_service: "FlociGCPService", floci_gcs_bucket_name: str, floci_gcs_obstore: "ObjectStoreProtocol" +) -> None: + """Verify ObStoreBackend and StoragePipeline streaming and Arrow round-trips on Floci GCS.""" + await _assert_backend_streaming_and_arrow(floci_gcs_obstore, "streaming/floci_gcs_obstore") + + registry = StorageRegistry() + registry.register_alias( + "floci-gcs-stream", + f"gs://{floci_gcs_bucket_name}", + backend="obstore", + **floci_gcs_obstore_kwargs(floci_gcp_service), + ) + object_path = "streaming/pipeline_gcs.bin" + floci_gcs_obstore.write_bytes_sync(object_path, STREAM_PAYLOAD) + + sync_pipeline = SyncStoragePipeline(registry=registry) + sync_chunks = list( + sync_pipeline.stream_read(f"alias://floci-gcs-stream/{object_path}", chunk_size=STREAM_CHUNK_SIZE) + ) + assert len(sync_chunks) > 1 + assert b"".join(sync_chunks) == STREAM_PAYLOAD + + async_pipeline = AsyncStoragePipeline(registry=registry) + async_stream = await async_pipeline.stream_read_async( + f"alias://floci-gcs-stream/{object_path}", chunk_size=STREAM_CHUNK_SIZE + ) + async_chunks = [chunk async for chunk in async_stream] + assert b"".join(async_chunks) == STREAM_PAYLOAD + + +@pytest.mark.xdist_group("floci_gcp") +async def test_floci_gcs_fsspec_streaming(floci_gcs_fsspec: "ObjectStoreProtocol") -> None: + """Verify FSSpecBackend streaming and Arrow round-trips on Floci GCS.""" + await _assert_backend_streaming_and_arrow(floci_gcs_fsspec, "streaming/floci_gcs_fsspec") + + +@pytest.mark.xdist_group("azure_blob") +async def test_azurite_obstore_streaming_and_pipeline( + azure_blob_service: "AzureBlobService", + azure_blob_default_container_name: str, + azurite_obstore: "ObjectStoreProtocol", +) -> None: + """Verify ObStoreBackend and StoragePipeline streaming and Arrow round-trips on Azurite.""" + await _assert_backend_streaming_and_arrow(azurite_obstore, "streaming/azurite_obstore") + + registry = StorageRegistry() + registry.register_alias( + "azurite-stream", + f"az://{azure_blob_default_container_name}", + backend="obstore", + account_name=azure_blob_service.account_name, + account_key=azure_blob_service.account_key, + endpoint=azure_blob_service.account_url, + client_options={"allow_http": True}, + ) + object_path = "streaming/pipeline_az.bin" + azurite_obstore.write_bytes_sync(object_path, STREAM_PAYLOAD) + + sync_pipeline = SyncStoragePipeline(registry=registry) + sync_chunks = list(sync_pipeline.stream_read(f"alias://azurite-stream/{object_path}", chunk_size=STREAM_CHUNK_SIZE)) + assert len(sync_chunks) > 1 + assert b"".join(sync_chunks) == STREAM_PAYLOAD + + +@pytest.mark.xdist_group("azure_blob") +async def test_azurite_fsspec_streaming(azurite_fsspec: "ObjectStoreProtocol") -> None: + """Verify FSSpecBackend streaming and Arrow round-trips on Azurite.""" + await _assert_backend_streaming_and_arrow(azurite_fsspec, "streaming/azurite_fsspec") + + +async def _assert_backend_streaming_and_arrow(backend: "ObjectStoreProtocol", prefix: str) -> None: + """Assert sync/async chunked byte streaming and Arrow Parquet round-trip on an object store backend.""" + blob_path = f"{prefix}/payload.bin" + backend.write_bytes_sync(blob_path, STREAM_PAYLOAD) + + sync_chunks = list(backend.stream_read_sync(blob_path, chunk_size=STREAM_CHUNK_SIZE)) + assert len(sync_chunks) > 1 + assert b"".join(sync_chunks) == STREAM_PAYLOAD + + async_stream = await backend.stream_read_async(blob_path, chunk_size=STREAM_CHUNK_SIZE) + async_chunks = [chunk async for chunk in async_stream] + assert len(async_chunks) >= 1 + assert b"".join(async_chunks) == STREAM_PAYLOAD + + if PYARROW_INSTALLED and pyarrow is not None: + arrow_path = f"{prefix}/records.parquet" + expected_table = pyarrow.table({"id": [1, 2, 3], "name": ["alpha", "beta", "gamma"]}) + backend.write_arrow_sync(arrow_path, expected_table) + loaded_table = backend.read_arrow_sync(arrow_path) + assert loaded_table.equals(expected_table) From 4a58c08958c949d299433d7525c20077e1dfd42d Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 15:00:21 +0000 Subject: [PATCH 13/35] fix(postgres): use enable_columnar_scan GUC and add alloydb omni 18 and mariadb 11.8 suites --- sqlspec/adapters/asyncpg/config.py | 2 +- sqlspec/adapters/asyncpg/driver.py | 2 +- sqlspec/adapters/psqlpy/config.py | 11 +- sqlspec/adapters/psqlpy/driver.py | 2 +- sqlspec/adapters/psycopg/config.py | 4 +- sqlspec/adapters/psycopg/driver.py | 4 +- .../adapters/mysql/test_mariadb.py | 123 ++++++++ .../adapters/postgres/test_alloydb_omni.py | 34 +++ tests/integration/fixtures/mysql.py | 145 +++++++++- tests/integration/fixtures/postgres.py | 268 +++++++++++++++++- .../unit/adapters/test_asyncpg/test_config.py | 6 +- .../unit/adapters/test_psqlpy/test_config.py | 14 +- .../unit/adapters/test_psycopg/test_config.py | 10 +- 13 files changed, 592 insertions(+), 33 deletions(-) create mode 100644 tests/integration/adapters/mysql/test_mariadb.py create mode 100644 tests/integration/adapters/postgres/test_alloydb_omni.py diff --git a/sqlspec/adapters/asyncpg/config.py b/sqlspec/adapters/asyncpg/config.py index 9d594d5ca..19db0a191 100644 --- a/sqlspec/adapters/asyncpg/config.py +++ b/sqlspec/adapters/asyncpg/config.py @@ -357,7 +357,7 @@ def __init__( normalized_conn_cfg.setdefault("statement_cache_size", 0) if bool(features_dict.get("enable_alloydb_columnar", False)) and features_dict.get("pooler_mode") == "session": merged_settings = dict(self._explicit_server_settings) if self._explicit_server_settings is not None else {} - merged_settings.setdefault("google_columnar_engine.enabled", "on") + merged_settings.setdefault("google_columnar_engine.enable_columnar_scan", "on") normalized_conn_cfg["server_settings"] = merged_settings self._custom_type_codecs: list[dict[str, Any]] = list(features_dict.get("type_codecs") or []) diff --git a/sqlspec/adapters/asyncpg/driver.py b/sqlspec/adapters/asyncpg/driver.py index 4836681b2..307e35e54 100644 --- a/sqlspec/adapters/asyncpg/driver.py +++ b/sqlspec/adapters/asyncpg/driver.py @@ -233,7 +233,7 @@ async def begin(self) -> None: try: await transaction.start() if self._should_set_local_columnar(): - await self.connection.execute("SET LOCAL google_columnar_engine.enabled = 'on'") + await self.connection.execute("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") except AsyncpgPostgresError as e: self._release_failed_transaction_claim(transaction) raise create_mapped_exception(e) from e diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py index 5dd3147ee..b1a0b71fb 100644 --- a/sqlspec/adapters/psqlpy/config.py +++ b/sqlspec/adapters/psqlpy/config.py @@ -277,7 +277,6 @@ def __init__( **kwargs: Additional keyword arguments. """ raw_conn_cfg = normalize_connection_config(connection_config) - self._explicit_max_cached_statement_lifetime = "max_cached_statement_lifetime" in raw_conn_cfg raw_options = raw_conn_cfg.get("options") self._explicit_options: str | None = raw_options if isinstance(raw_options, str) else None connection_config = build_connection_config(raw_conn_cfg) @@ -286,15 +285,11 @@ def __init__( statement_config, driver_features = apply_driver_features(statement_config, driver_features) features_dict = dict(driver_features) if driver_features else {} - if features_dict.get("pooler_mode") in {"transaction", "statement"} or bool( - features_dict.get("pgbouncer") or features_dict.get("pgbouncer_mode") - ): - connection_config.setdefault("max_cached_statement_lifetime", 0) if bool(features_dict.get("enable_alloydb_columnar", False)) and features_dict.get("pooler_mode") == "session": - columnar_opt = "-c google_columnar_engine.enabled=on" + columnar_opt = "-c google_columnar_engine.enable_columnar_scan=on" existing_options = connection_config.get("options") if isinstance(existing_options, str) and existing_options.strip(): - if "google_columnar_engine.enabled" not in existing_options: + if "google_columnar_engine.enable_columnar_scan" not in existing_options: connection_config["options"] = f"{existing_options} {columnar_opt}" else: connection_config["options"] = columnar_opt @@ -402,8 +397,6 @@ def derive_direct_config(self) -> Self: if not direct_dsn: return self conn_cfg: dict[str, Any] = dict(self.connection_config) - if not self._explicit_max_cached_statement_lifetime and conn_cfg.get("max_cached_statement_lifetime") == 0: - conn_cfg.pop("max_cached_statement_lifetime", None) if self._explicit_options is not None: conn_cfg["options"] = self._explicit_options else: diff --git a/sqlspec/adapters/psqlpy/driver.py b/sqlspec/adapters/psqlpy/driver.py index 0862dea01..5df67ce27 100644 --- a/sqlspec/adapters/psqlpy/driver.py +++ b/sqlspec/adapters/psqlpy/driver.py @@ -228,7 +228,7 @@ async def begin(self) -> None: try: await self.connection.execute("BEGIN") if self._should_set_local_columnar(): - await self.connection.execute("SET LOCAL google_columnar_engine.enabled = 'on'") + await self.connection.execute("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") except (PsqlpyDatabaseError, PsqlpyConnectionExecuteError, PsqlpyError) as e: msg = f"Failed to begin psqlpy transaction: {e}" raise SQLSpecError(msg) from e diff --git a/sqlspec/adapters/psycopg/config.py b/sqlspec/adapters/psycopg/config.py index d1f8bcc6d..8643fb419 100644 --- a/sqlspec/adapters/psycopg/config.py +++ b/sqlspec/adapters/psycopg/config.py @@ -358,7 +358,7 @@ def __init__( connection_config.setdefault("prepare_threshold", None) if features_dict.get("enable_alloydb_columnar") and not is_transaction_pooler: existing_options = connection_config.get("options") - columnar_opt = "-c google_columnar_engine.enabled=on" + columnar_opt = "-c google_columnar_engine.enable_columnar_scan=on" if not existing_options: connection_config["options"] = columnar_opt elif columnar_opt not in str(existing_options): @@ -806,7 +806,7 @@ def __init__( connection_config.setdefault("prepare_threshold", None) if features_dict.get("enable_alloydb_columnar") and not is_transaction_pooler: existing_options = connection_config.get("options") - columnar_opt = "-c google_columnar_engine.enabled=on" + columnar_opt = "-c google_columnar_engine.enable_columnar_scan=on" if not existing_options: connection_config["options"] = columnar_opt elif columnar_opt not in str(existing_options): diff --git a/sqlspec/adapters/psycopg/driver.py b/sqlspec/adapters/psycopg/driver.py index 4880612da..a400ac40a 100644 --- a/sqlspec/adapters/psycopg/driver.py +++ b/sqlspec/adapters/psycopg/driver.py @@ -388,7 +388,7 @@ def begin(self) -> None: if restore_autocommit: self.connection.autocommit = False if self._should_set_local_columnar(): - self.connection.execute("SET LOCAL google_columnar_engine.enabled = 'on'") + self.connection.execute("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") except psycopg.Error as e: mapped = create_mapped_exception(e) if type(mapped) is not SQLSpecError: @@ -915,7 +915,7 @@ async def begin(self) -> None: if restore_autocommit: await self.connection.set_autocommit(False) if self._should_set_local_columnar(): - await self.connection.execute("SET LOCAL google_columnar_engine.enabled = 'on'") + await self.connection.execute("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") except psycopg.Error as e: mapped = create_mapped_exception(e) if type(mapped) is not SQLSpecError: diff --git a/tests/integration/adapters/mysql/test_mariadb.py b/tests/integration/adapters/mysql/test_mariadb.py new file mode 100644 index 000000000..6e615bcb7 --- /dev/null +++ b/tests/integration/adapters/mysql/test_mariadb.py @@ -0,0 +1,123 @@ +"""Live MariaDB 11.8 compatibility integration tests across MySQL drivers.""" + +from typing import TYPE_CHECKING, Any + +import pytest + +from sqlspec.utils.serializers import from_json + +if TYPE_CHECKING: + from tests.integration.fixtures.mysql import MariadbDriverSession + +pytestmark = [pytest.mark.mysql, pytest.mark.integration, pytest.mark.xdist_group("mariadb")] + + +async def test_mariadb_118_version_and_parameterized_crud(mariadb_driver_session: "MariadbDriverSession") -> None: + """Verify MariaDB 11.8 version banner and parameterized CRUD operations across MySQL drivers.""" + version_banner = await mariadb_driver_session.select_version() + assert "11.8" in version_banner + assert "mariadb" in version_banner.lower() + + table_name = mariadb_driver_session.table_name + await mariadb_driver_session.execute_ddl( + f""" + CREATE TABLE {table_name} ( + id INT PRIMARY KEY, + name VARCHAR(128) NOT NULL, + payload JSON NOT NULL + ) ENGINE=InnoDB + """ + ) + + await mariadb_driver_session.execute_dml( + f"INSERT INTO {table_name} (id, name, payload) VALUES (:id, :name, :payload)", + id=1, + name="initial", + payload={"tier": "primary", "count": 1}, + ) + await mariadb_driver_session.execute_dml( + f"UPDATE {table_name} SET name = :name WHERE id = :id", id=1, name="updated" + ) + + rows = await mariadb_driver_session.select_rows( + f"SELECT id AS id, name AS name FROM {table_name} WHERE id = :id", id=1 + ) + assert rows == [{"id": 1, "name": "updated"}] + + await mariadb_driver_session.execute_dml(f"DELETE FROM {table_name} WHERE id = :id", id=1) + remaining = await mariadb_driver_session.select_rows(f"SELECT id AS id FROM {table_name}") + assert remaining == [] + + +async def test_mariadb_118_transaction_commit_and_rollback(mariadb_driver_session: "MariadbDriverSession") -> None: + """Verify explicit transaction rollback and commit semantics against MariaDB 11.8.""" + table_name = mariadb_driver_session.table_name + await mariadb_driver_session.execute_ddl( + f""" + CREATE TABLE {table_name} ( + id INT PRIMARY KEY, + name VARCHAR(128) NOT NULL + ) ENGINE=InnoDB + """ + ) + + await mariadb_driver_session.insert_and_rollback(table_name, row_id=10, name="discarded") + after_rollback = await mariadb_driver_session.select_rows(f"SELECT id AS id, name AS name FROM {table_name}") + assert after_rollback == [] + + await mariadb_driver_session.insert_and_commit(table_name, row_id=20, name="persisted") + after_commit = await mariadb_driver_session.select_rows(f"SELECT id AS id, name AS name FROM {table_name}") + assert after_commit == [{"id": 20, "name": "persisted"}] + + +async def test_mariadb_118_json_roundtrip_and_on_duplicate_key_upsert( + mariadb_driver_session: "MariadbDriverSession", +) -> None: + """Verify JSON payload round-trip and ON DUPLICATE KEY UPDATE VALUES(col) upsert on MariaDB 11.8.""" + table_name = mariadb_driver_session.table_name + await mariadb_driver_session.execute_ddl( + f""" + CREATE TABLE {table_name} ( + id INT PRIMARY KEY, + name VARCHAR(128) NOT NULL, + payload JSON NOT NULL + ) ENGINE=InnoDB + """ + ) + + upsert_sql = f""" + INSERT INTO {table_name} (id, name, payload) + VALUES (:id, :name, :payload) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + payload = VALUES(payload) + """ + first_payload: dict[str, Any] = {"engine": "mariadb", "tags": ["lts", "11.8"], "revision": 1} + second_payload: dict[str, Any] = {"engine": "mariadb", "tags": ["lts", "11.8", "upsert"], "revision": 2} + + await mariadb_driver_session.execute_dml(upsert_sql, id=7, name="v1", payload=first_payload) + await mariadb_driver_session.execute_dml(upsert_sql, id=7, name="v2", payload=second_payload) + + rows = await mariadb_driver_session.select_rows( + f""" + SELECT + id AS id, + name AS name, + payload AS payload, + JSON_VALUE(payload, '$.engine') AS engine_name, + CAST(JSON_VALUE(payload, '$.revision') AS SIGNED) AS revision_num + FROM {table_name} + WHERE id = :id + """, + id=7, + ) + assert len(rows) == 1 + row = rows[0] + assert row["id"] == 7 + assert row["name"] == "v2" + assert row["engine_name"] == "mariadb" + assert int(row["revision_num"]) == 2 + + raw_payload = row["payload"] + decoded_payload = from_json(raw_payload) if isinstance(raw_payload, (str, bytes)) else raw_payload + assert decoded_payload == second_payload diff --git a/tests/integration/adapters/postgres/test_alloydb_omni.py b/tests/integration/adapters/postgres/test_alloydb_omni.py new file mode 100644 index 000000000..635db6b34 --- /dev/null +++ b/tests/integration/adapters/postgres/test_alloydb_omni.py @@ -0,0 +1,34 @@ +"""Live AlloyDB Omni 18 integration tests for enable_alloydb_columnar across PostgreSQL drivers.""" + +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from tests.integration.fixtures.postgres import AlloydbOmniHarness + +pytestmark = [pytest.mark.postgres, pytest.mark.integration, pytest.mark.xdist_group("alloydb")] + + +async def test_alloydb_omni_session_columnar_guc_and_crud(alloydb_omni_harness: "AlloydbOmniHarness") -> None: + """Verify enable_alloydb_columnar=True activates google_columnar_engine.enable_columnar_scan in session mode.""" + engine_enabled, scan_enabled = await alloydb_omni_harness.show_session_columnar_settings() + assert engine_enabled == "on" + assert scan_enabled == "on" + + rows = await alloydb_omni_harness.verify_session_ddl_and_dml() + assert rows == [{"id": 1, "label": "alpha", "metric": 10}, {"id": 2, "label": "beta", "metric": 25}] + + +async def test_alloydb_omni_transaction_pooler_columnar_guc(alloydb_omni_harness: "AlloydbOmniHarness") -> None: + """Verify pooler_mode='transaction' applies SET LOCAL google_columnar_engine.enable_columnar_scan inside begin().""" + in_tx_scan, after_commit_scan = await alloydb_omni_harness.show_transaction_columnar_transition() + assert in_tx_scan == "on" + assert after_commit_scan == "off" + + +async def test_alloydb_omni_vector_extension_roundtrip(alloydb_omni_harness: "AlloydbOmniHarness") -> None: + """Verify AlloyDB Omni 18 exposes the vector extension and executes L2 distance queries.""" + extension_name, distance = await alloydb_omni_harness.verify_vector_extension() + assert extension_name == "vector" + assert distance == pytest.approx(0.0) diff --git a/tests/integration/fixtures/mysql.py b/tests/integration/fixtures/mysql.py index 1c2ef582c..6a6f3b052 100644 --- a/tests/integration/fixtures/mysql.py +++ b/tests/integration/fixtures/mysql.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator, Generator + from pytest_databases.docker.mariadb import MariaDBService from pytest_databases.docker.mysql import MySQLService from sqlspec.adapters.aiomysql import AiomysqlConfig, AiomysqlDriver @@ -61,6 +62,7 @@ PyMysqlADKStore = import_optional_attr("sqlspec.adapters.pymysql.adk", "PyMysqlADKStore") __all__ = ( + "MariadbDriverSession", "MysqlAdkResidualContext", "aiomysql_clean_driver", "aiomysql_config", @@ -68,6 +70,7 @@ "asyncmy_clean_driver", "asyncmy_config", "asyncmy_driver", + "mariadb_driver_session", "mysql_adk_residual_context", "mysqlconnector_async_config", "mysqlconnector_async_driver", @@ -155,6 +158,68 @@ async def execute_sql(self, config: Any, sql: str, **params: Any) -> None: await _execute_mysql_sql(config, sql, is_async=self.is_async, **params) +class MariadbDriverSession(msgspec.Struct, frozen=True, gc=False): + """Cross-driver test harness for MariaDB 11.8 CRUD, transactions, JSON, and upsert checks.""" + + driver_mode: str + is_async: bool + config: Any + tx_config: Any + table_name: str + + async def select_version(self) -> str: + """Return the active MariaDB server version string.""" + if self.is_async: + async with self.config.provide_session() as driver: + return str(await driver.select_value("SELECT VERSION()")) + with self.config.provide_session() as driver: + return str(driver.select_value("SELECT VERSION()")) + + async def execute_ddl(self, sql: str) -> None: + """Execute a DDL statement against MariaDB 11.8.""" + await _execute_mysql_sql(self.config, sql, is_async=self.is_async) + + async def execute_dml(self, sql: str, **params: Any) -> None: + """Execute a parameterized DML statement and commit.""" + await _execute_mysql_sql(self.config, sql, is_async=self.is_async, **params) + + async def select_rows(self, sql: str, **params: Any) -> list[dict[str, Any]]: + """Execute a parameterized SELECT statement and return dict rows.""" + if self.is_async: + async with self.config.provide_session() as driver: + return cast("list[dict[str, Any]]", await driver.select(sql, **params)) + with self.config.provide_session() as driver: + return cast("list[dict[str, Any]]", driver.select(sql, **params)) + + async def insert_and_rollback(self, table_name: str, *, row_id: int, name: str) -> None: + """Insert a row inside an explicit transaction and roll back.""" + insert_sql = f"INSERT INTO {table_name} (id, name) VALUES (:id, :name)" + if self.is_async: + async with self.tx_config.provide_session() as driver: + await driver.begin() + await driver.execute(insert_sql, id=row_id, name=name) + await driver.rollback() + return + with self.tx_config.provide_session() as driver: + driver.begin() + driver.execute(insert_sql, id=row_id, name=name) + driver.rollback() + + async def insert_and_commit(self, table_name: str, *, row_id: int, name: str) -> None: + """Insert a row inside an explicit transaction and commit.""" + insert_sql = f"INSERT INTO {table_name} (id, name) VALUES (:id, :name)" + if self.is_async: + async with self.tx_config.provide_session() as driver: + await driver.begin() + await driver.execute(insert_sql, id=row_id, name=name) + await driver.commit() + return + with self.tx_config.provide_session() as driver: + driver.begin() + driver.execute(insert_sql, id=row_id, name=name) + driver.commit() + + @pytest.fixture(scope="session") async def asyncmy_config(mysql_service: "MySQLService") -> "AsyncGenerator[AsyncmyConfig, None]": """Provide a session-scoped AsyncmyConfig with a shared pool.""" @@ -444,7 +509,31 @@ async def mysql_adk_residual_context( await _close_mysql_config(config, is_async=is_async) -def _mysql_connection_config(mysql_service: "MySQLService", *, database_key: str = "database") -> "dict[str, Any]": +@pytest.fixture(params=("aiomysql", "asyncmy", "mysqlconnector-async", "mysqlconnector-sync", "pymysql")) +async def mariadb_driver_session( + request: pytest.FixtureRequest, mariadb_118_service: "MariaDBService" +) -> "AsyncGenerator[MariadbDriverSession, None]": + """Provide isolated driver sessions explicitly targeting MariaDB 11.8 across MySQL drivers.""" + driver_mode = str(request.param) + pytest.importorskip(_MYSQL_ADK_DRIVER_PACKAGES[driver_mode]) + + table_name = f"mariadb_compat_{uuid4().hex[:8]}" + is_async, config, tx_config = _build_mariadb_configs(mariadb_118_service, driver_mode) + + try: + yield MariadbDriverSession( + driver_mode=driver_mode, is_async=is_async, config=config, tx_config=tx_config, table_name=table_name + ) + finally: + await _close_mysql_config(tx_config, is_async=is_async) + with contextlib.suppress(Exception): + await _execute_mysql_sql(config, f"DROP TABLE IF EXISTS {table_name}", is_async=is_async) + await _close_mysql_config(config, is_async=is_async) + + +def _mysql_connection_config( + mysql_service: "MySQLService | MariaDBService", *, database_key: str = "database" +) -> "dict[str, Any]": return { "host": mysql_service.host, "port": mysql_service.port, @@ -455,6 +544,60 @@ def _mysql_connection_config(mysql_service: "MySQLService", *, database_key: str } +def _build_mariadb_configs(service: "MariaDBService", driver_mode: str) -> tuple[bool, Any, Any]: + if driver_mode == "aiomysql": + conn_cfg = _mysql_connection_config(service, database_key="db") + conn_cfg.update({"minsize": 1, "maxsize": 3}) + tx_cfg = dict(conn_cfg) + tx_cfg["autocommit"] = False + return ( + True, + AiomysqlConfig(connection_config=conn_cfg, statement_config=aiomysql_statement_config), + AiomysqlConfig(connection_config=tx_cfg, statement_config=aiomysql_statement_config), + ) + if driver_mode == "asyncmy": + conn_cfg = _mysql_connection_config(service) + conn_cfg.update({"minsize": 1, "maxsize": 3}) + tx_cfg = dict(conn_cfg) + tx_cfg["autocommit"] = False + return ( + True, + AsyncmyConfig(connection_config=conn_cfg, statement_config=asyncmy_statement_config), + AsyncmyConfig(connection_config=tx_cfg, statement_config=asyncmy_statement_config), + ) + if driver_mode == "mysqlconnector-async": + conn_cfg = _mysql_connection_config(service) + conn_cfg["use_pure"] = True + tx_cfg = dict(conn_cfg) + tx_cfg["autocommit"] = False + return ( + True, + MysqlConnectorAsyncConfig(connection_config=conn_cfg, statement_config=mysqlconnector_statement_config), + MysqlConnectorAsyncConfig(connection_config=tx_cfg, statement_config=mysqlconnector_statement_config), + ) + if driver_mode == "mysqlconnector-sync": + conn_cfg = _mysql_connection_config(service) + conn_cfg.update({"use_pure": True, "pool_size": 3}) + tx_cfg = dict(conn_cfg) + tx_cfg["autocommit"] = False + return ( + False, + MysqlConnectorSyncConfig(connection_config=conn_cfg, statement_config=mysqlconnector_statement_config), + MysqlConnectorSyncConfig(connection_config=tx_cfg, statement_config=mysqlconnector_statement_config), + ) + if driver_mode == "pymysql": + conn_cfg = _mysql_connection_config(service) + tx_cfg = dict(conn_cfg) + tx_cfg["autocommit"] = False + return ( + False, + PyMysqlConfig(connection_config=conn_cfg, statement_config=pymysql_statement_config), + PyMysqlConfig(connection_config=tx_cfg, statement_config=pymysql_statement_config), + ) + msg = f"Unsupported MariaDB driver mode: {driver_mode}" + raise ValueError(msg) + + async def _run_async_cleanup(driver: Any, adapter_suffix: str, *, procedure_suffix: str | None = None) -> None: for statement in mysql_cleanup_statements(adapter_suffix, procedure_suffix=procedure_suffix): await driver.execute_script(statement) diff --git a/tests/integration/fixtures/postgres.py b/tests/integration/fixtures/postgres.py index dec53c1b5..8a542a215 100644 --- a/tests/integration/fixtures/postgres.py +++ b/tests/integration/fixtures/postgres.py @@ -1,15 +1,22 @@ """Shared PostgreSQL-family integration fixtures.""" -from typing import TYPE_CHECKING, Any +import contextlib +from typing import TYPE_CHECKING, Any, cast +from uuid import uuid4 +import msgspec import pytest -from pytest_databases.docker.postgres import PostgresService, exec_psql +from pytest_databases.docker.postgres import PostgresService, create_worker_database, exec_psql +from pytest_databases.helpers import get_xdist_worker_num from sqlspec.utils.module_loader import import_optional_attr if TYPE_CHECKING: from collections.abc import AsyncGenerator, Generator + from pytest_databases import DockerService + from pytest_databases.types import ServiceContainer, XdistIsolationLevel + from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver from sqlspec.adapters.psqlpy import PsqlpyConfig, PsqlpyDriver, PsqlpyPoolParams @@ -27,9 +34,12 @@ PsycopgSyncConfig = import_optional_attr("sqlspec.adapters.psycopg", "PsycopgSyncConfig") __all__ = ( + "AlloydbOmniHarness", "adbc_postgres_config", "adbc_postgres_connection_config", "adbc_sync_driver", + "alloydb_omni_18_service", + "alloydb_omni_harness", "asyncpg_async_driver", "asyncpg_config", "asyncpg_connection_config", @@ -49,6 +59,90 @@ ) +class AlloydbOmniHarness(msgspec.Struct, frozen=True, gc=False): + """Cross-driver test harness for AlloyDB Omni 18 columnar and vector execution.""" + + driver_mode: str + is_async: bool + service: PostgresService + session_config: Any + transaction_config: Any + table_name: str + + async def show_session_columnar_settings(self) -> tuple[str, str]: + """Return (google_columnar_engine.enabled, google_columnar_engine.enable_columnar_scan) in session mode.""" + if self.is_async: + async with self.session_config.provide_session() as driver: + enabled = await driver.select_value("SHOW google_columnar_engine.enabled") + scan_enabled = await driver.select_value("SHOW google_columnar_engine.enable_columnar_scan") + return str(enabled), str(scan_enabled) + with self.session_config.provide_session() as driver: + enabled = driver.select_value("SHOW google_columnar_engine.enabled") + scan_enabled = driver.select_value("SHOW google_columnar_engine.enable_columnar_scan") + return str(enabled), str(scan_enabled) + + async def verify_session_ddl_and_dml(self) -> list[dict[str, Any]]: + """Create a scratch table, insert parameterized rows, and read them back.""" + create_sql = ( + f"CREATE TABLE {self.table_name} (" + "id INTEGER PRIMARY KEY, " + "label VARCHAR(64) NOT NULL, " + "metric INTEGER NOT NULL" + ")" + ) + insert_sql = f"INSERT INTO {self.table_name} (id, label, metric) VALUES ($1, $2, $3)" + select_sql = f"SELECT id, label, metric FROM {self.table_name} ORDER BY id" + if self.is_async: + async with self.session_config.provide_session() as driver: + await driver.execute(create_sql) + await driver.execute(insert_sql, (1, "alpha", 10)) + await driver.execute(insert_sql, (2, "beta", 25)) + await driver.commit() + return cast("list[dict[str, Any]]", await driver.select(select_sql)) + with self.session_config.provide_session() as driver: + driver.execute(create_sql) + driver.execute(insert_sql, (1, "alpha", 10)) + driver.execute(insert_sql, (2, "beta", 25)) + driver.commit() + return cast("list[dict[str, Any]]", driver.select(select_sql)) + + async def show_transaction_columnar_transition(self) -> tuple[str, str]: + """Return google_columnar_engine.enable_columnar_scan inside begin() and after commit().""" + guc_sql = "SHOW google_columnar_engine.enable_columnar_scan" + if self.is_async: + async with self.transaction_config.provide_session() as driver: + await driver.begin() + try: + in_tx = await driver.select_value(guc_sql) + finally: + await driver.commit() + after_commit = await driver.select_value(guc_sql) + return str(in_tx), str(after_commit) + with self.transaction_config.provide_session() as driver: + driver.begin() + try: + in_tx = driver.select_value(guc_sql) + finally: + driver.commit() + after_commit = driver.select_value(guc_sql) + return str(in_tx), str(after_commit) + + async def verify_vector_extension(self) -> tuple[str, float]: + """Enable the AlloyDB Omni vector extension and compute L2 distance.""" + _ensure_postgres_extension(self.service, "vector") + ext_sql = "SELECT extname FROM pg_extension WHERE extname = $1" + dist_sql = "SELECT ('[1,2,3]'::vector <-> '[1,2,3]'::vector)::float8" + if self.is_async: + async with self.session_config.provide_session() as driver: + ext_name = await driver.select_value(ext_sql, ("vector",)) + distance = await driver.select_value(dist_sql) + return str(ext_name), float(distance) + with self.session_config.provide_session() as driver: + ext_name = driver.select_value(ext_sql, ("vector",)) + distance = driver.select_value(dist_sql) + return str(ext_name), float(distance) + + @pytest.fixture(scope="session") def asyncpg_connection_config(postgres_service: PostgresService) -> dict[str, Any]: """Base pool configuration for AsyncPG tests.""" @@ -266,6 +360,176 @@ def paradedb_config_psycopg(paradedb_service: PostgresService) -> "Generator[Psy config.connection_instance = None +@pytest.fixture(scope="session") +def alloydb_omni_18_service( + docker_service: "DockerService", + xdist_postgres_isolation_level: "XdistIsolationLevel", + postgres_host: str, + postgres_user: str, + postgres_password: str, + alloydb_omni_18_port: int | None, +) -> "Generator[PostgresService, None, None]": + """Provide an AlloyDB Omni 18 service with an extended initdb startup timeout. + + AlloyDB Omni preloads ``g_columnar_engine``, ``g_stats``, ``google_job_scheduler``, + ``google_ml_integration``, and ``google_storage`` during cold ``initdb``, which + takes ~15-20s and exceeds ``provide_postgres_service``'s 10s default timeout. + """ + + def check(service_container: "ServiceContainer") -> bool: + exit_code, output = exec_psql( + service_container.container, + user=postgres_user, + password=postgres_password, + database="postgres", + sql="SELECT 1", + tuples_only=True, + ) + return exit_code == 0 and output.strip() == b"1" + + name = "alloydb-omni-18" + db_name = "pytest_databases" + worker_num = get_xdist_worker_num() + if worker_num is not None: + suffix = f"_{worker_num}" + if xdist_postgres_isolation_level == "server": + name += suffix + else: + db_name += suffix + + with docker_service.run( + image="gcr.io/alloydb-omni/alloydbomni:18", + command=[ + "postgres", + "-c", + "google_columnar_engine.enabled=on", + "-c", + "google_columnar_engine.enable_columnar_scan=off", + ], + check=check, + container_host=postgres_host, + container_port=5432, + name=name, + env={"POSTGRES_PASSWORD": postgres_password}, + transient=xdist_postgres_isolation_level == "server", + host_port=alloydb_omni_18_port, + timeout=90, + pause=0.5, + ) as service: + create_worker_database(service.container, user=postgres_user, password=postgres_password, db_name=db_name) + yield PostgresService( + host=service.host, + port=service.port, + container=service.container, + database=db_name, + user=postgres_user, + password=postgres_password, + ) + + +@pytest.fixture(params=("asyncpg", "psycopg-sync", "psycopg-async", "psqlpy")) +async def alloydb_omni_harness( + request: pytest.FixtureRequest, alloydb_omni_18_service: PostgresService +) -> "AsyncGenerator[AlloydbOmniHarness, None]": + """Provide session-mode and transaction-pooler-mode configs targeting AlloyDB Omni 18.""" + driver_mode = str(request.param) + _require_alloydb_driver(driver_mode) + + is_async, session_config = _build_alloydb_omni_config( + alloydb_omni_18_service, driver_mode, driver_features={"enable_alloydb_columnar": True} + ) + _, transaction_config = _build_alloydb_omni_config( + alloydb_omni_18_service, + driver_mode, + driver_features={"enable_alloydb_columnar": True, "pooler_mode": "transaction"}, + ) + table_name = f"alloydb_col_{uuid4().hex[:8]}" + + try: + yield AlloydbOmniHarness( + driver_mode=driver_mode, + is_async=is_async, + service=alloydb_omni_18_service, + session_config=session_config, + transaction_config=transaction_config, + table_name=table_name, + ) + finally: + await _close_postgres_config(transaction_config, driver_mode=driver_mode) + with contextlib.suppress(Exception): + await _drop_postgres_table(session_config, table_name, is_async=is_async) + await _close_postgres_config(session_config, driver_mode=driver_mode) + + +def _require_alloydb_driver(driver_mode: str) -> None: + if driver_mode == "asyncpg": + pytest.importorskip("asyncpg") + return + if driver_mode in {"psycopg-sync", "psycopg-async"}: + pytest.importorskip("psycopg") + pytest.importorskip("psycopg_pool") + return + if driver_mode == "psqlpy": + pytest.importorskip("psqlpy") + return + msg = f"Unsupported AlloyDB Omni driver mode: {driver_mode}" + raise ValueError(msg) + + +def _build_alloydb_omni_config( + service: PostgresService, driver_mode: str, *, driver_features: dict[str, Any] +) -> tuple[bool, Any]: + if driver_mode == "asyncpg": + conn_cfg = _postgres_connection_config(service) + conn_cfg.update({"min_size": 1, "max_size": 3}) + return True, AsyncpgConfig(connection_config=conn_cfg, driver_features=dict(driver_features)) + if driver_mode == "psycopg-sync": + return False, PsycopgSyncConfig( + connection_config={"conninfo": _postgres_conninfo(service), "min_size": 1, "max_size": 3}, + driver_features=dict(driver_features), + ) + if driver_mode == "psycopg-async": + return True, PsycopgAsyncConfig( + connection_config={"conninfo": _postgres_conninfo(service), "min_size": 1, "max_size": 3}, + driver_features=dict(driver_features), + ) + if driver_mode == "psqlpy": + return True, PsqlpyConfig( + connection_config={"dsn": _psqlpy_dsn(service), "max_db_pool_size": 3}, + driver_features=dict(driver_features), + ) + msg = f"Unsupported AlloyDB Omni driver mode: {driver_mode}" + raise ValueError(msg) + + +async def _drop_postgres_table(config: Any, table_name: str, *, is_async: bool) -> None: + drop_sql = f"DROP TABLE IF EXISTS {table_name}" + if is_async: + async with config.provide_session() as driver: + await driver.execute(drop_sql) + await driver.commit() + return + with config.provide_session() as driver: + driver.execute(drop_sql) + driver.commit() + + +async def _close_postgres_config(config: Any, *, driver_mode: str) -> None: + if config.connection_instance is None: + return + if driver_mode == "psqlpy": + config.connection_instance.close() + config.connection_instance = None + return + if driver_mode == "psycopg-sync": + config.close_pool() + config.connection_instance = None + return + with contextlib.suppress(RuntimeError): + await config.close_pool() + config.connection_instance = None + + def _postgres_connection_config(postgres_service: PostgresService) -> dict[str, Any]: return { "host": postgres_service.host, diff --git a/tests/unit/adapters/test_asyncpg/test_config.py b/tests/unit/adapters/test_asyncpg/test_config.py index 35e772ed3..ce81b2236 100644 --- a/tests/unit/adapters/test_asyncpg/test_config.py +++ b/tests/unit/adapters/test_asyncpg/test_config.py @@ -454,7 +454,7 @@ def test_asyncpg_derive_direct_config_uses_direct_dsn_and_session_mode() -> None assert "statement_cache_size" not in direct_config.connection_config assert direct_config.driver_features["pooler_mode"] == "session" assert direct_config.driver_features["direct_dsn"] is None - assert direct_config.connection_config["server_settings"] == {"google_columnar_engine.enabled": "on"} + assert direct_config.connection_config["server_settings"] == {"google_columnar_engine.enable_columnar_scan": "on"} mapping_config = AsyncpgConfig( connection_config={"host": "pooler.internal", "port": 6432, "database": "app"}, @@ -475,7 +475,7 @@ async def test_asyncpg_columnar_engine_session_vs_transaction() -> None: ) assert session_config.connection_config["server_settings"] == { "application_name": "sqlspec", - "google_columnar_engine.enabled": "on", + "google_columnar_engine.enable_columnar_scan": "on", } tx_config = AsyncpgConfig( @@ -493,4 +493,4 @@ async def test_asyncpg_columnar_engine_session_vs_transaction() -> None: ) await driver.begin() tx_handle.start.assert_awaited_once() - conn.execute.assert_awaited_once_with("SET LOCAL google_columnar_engine.enabled = 'on'") + conn.execute.assert_awaited_once_with("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") diff --git a/tests/unit/adapters/test_psqlpy/test_config.py b/tests/unit/adapters/test_psqlpy/test_config.py index 79cf21092..79703d08a 100644 --- a/tests/unit/adapters/test_psqlpy/test_config.py +++ b/tests/unit/adapters/test_psqlpy/test_config.py @@ -285,17 +285,20 @@ async def test_psqlpy_session_context_rolls_back_open_transaction_on_exit() -> N @pytest.mark.anyio async def test_psqlpy_pooler_mode_transaction_disables_prepared_statements() -> None: - """Transaction pooler mode should disable statement caching and pass prepared=False on execution.""" + """Transaction pooler mode should construct a valid ConnectionPool and pass prepared=False on execution.""" config = PsqlpyConfig( connection_config={"dsn": "postgresql://localhost:6432/test"}, driver_features={"pooler_mode": "transaction"} ) - assert config.connection_config["max_cached_statement_lifetime"] == 0 + assert config.driver_features["pooler_mode"] == "transaction" + pool = await config.create_pool() + pool.close() alias_config = PsqlpyConfig( connection_config={"dsn": "postgresql://localhost:6432/test"}, driver_features={"pgbouncer": True} ) assert alias_config.driver_features["pooler_mode"] == "transaction" - assert alias_config.connection_config["max_cached_statement_lifetime"] == 0 + alias_pool = await alias_config.create_pool() + alias_pool.close() conn = AsyncMock() conn.fetch.return_value = _ExtensionQueryResult(set()) @@ -338,10 +341,9 @@ async def test_psqlpy_derive_direct_config_and_columnar_engine() -> None: direct_config = pooled_config.derive_direct_config() assert direct_config is not pooled_config assert direct_config.connection_config["dsn"] == "postgresql://direct:5432/test" - assert "max_cached_statement_lifetime" not in direct_config.connection_config assert direct_config.driver_features["pooler_mode"] == "session" assert direct_config.driver_features["direct_dsn"] is None - assert direct_config.connection_config["options"] == "-c google_columnar_engine.enabled=on" + assert direct_config.connection_config["options"] == "-c google_columnar_engine.enable_columnar_scan=on" conn = AsyncMock() conn.in_transaction = False @@ -352,4 +354,4 @@ async def test_psqlpy_derive_direct_config_and_columnar_engine() -> None: ) await driver.begin() conn.execute.assert_any_await("BEGIN") - conn.execute.assert_any_await("SET LOCAL google_columnar_engine.enabled = 'on'") + conn.execute.assert_any_await("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") diff --git a/tests/unit/adapters/test_psycopg/test_config.py b/tests/unit/adapters/test_psycopg/test_config.py index 28a4c6e33..c1836b384 100644 --- a/tests/unit/adapters/test_psycopg/test_config.py +++ b/tests/unit/adapters/test_psycopg/test_config.py @@ -745,7 +745,7 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non driver_features={"enable_alloydb_columnar": True, "pooler_mode": "session"}, ) assert "-c statement_timeout=5000" in session_sync.connection_config["options"] - assert "-c google_columnar_engine.enabled=on" in session_sync.connection_config["options"] + assert "-c google_columnar_engine.enable_columnar_scan=on" in session_sync.connection_config["options"] assert session_sync.derive_direct_config() is session_sync txn_sync = PsycopgSyncConfig( @@ -765,7 +765,7 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non assert derived_sync.driver_features["pooler_mode"] == "session" assert derived_sync.driver_features["direct_dsn"] is None assert "prepare_threshold" not in derived_sync.connection_config - assert "-c google_columnar_engine.enabled=on" in derived_sync.connection_config["options"] + assert "-c google_columnar_engine.enable_columnar_scan=on" in derived_sync.connection_config["options"] txn_async = PsycopgAsyncConfig( connection_config={"conninfo": "postgresql://pooler:6432/db"}, @@ -781,7 +781,7 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non assert derived_async.driver_features["pooler_mode"] == "session" assert derived_async.driver_features["direct_dsn"] is None assert "prepare_threshold" not in derived_async.connection_config - assert "-c google_columnar_engine.enabled=on" in derived_async.connection_config["options"] + assert "-c google_columnar_engine.enable_columnar_scan=on" in derived_async.connection_config["options"] sync_conn = MagicMock() sync_conn.autocommit = True @@ -790,7 +790,7 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non connection=sync_conn, statement_config=txn_sync.statement_config, driver_features=txn_sync.driver_features ) sync_driver.begin() - sync_conn.execute.assert_called_once_with("SET LOCAL google_columnar_engine.enabled = 'on'") + sync_conn.execute.assert_called_once_with("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") async_conn = AsyncMock() async_conn.autocommit = True @@ -799,4 +799,4 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non connection=async_conn, statement_config=txn_async.statement_config, driver_features=txn_async.driver_features ) await async_driver.begin() - async_conn.execute.assert_awaited_once_with("SET LOCAL google_columnar_engine.enabled = 'on'") + async_conn.execute.assert_awaited_once_with("SET LOCAL google_columnar_engine.enable_columnar_scan = 'on'") From 4e889a3ea678dd66e0b64d7f2c02caa19652fcfa Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 17:25:53 +0000 Subject: [PATCH 14/35] test(adbc): wire containerized oracle, db2, snowflake, and bigquery adbc suites --- sqlspec/adapters/adbc/core.py | 73 +++- sqlspec/adapters/adbc/driver.py | 68 ++- .../adapters/_shared/adbc_connection.py | 37 +- tests/integration/adapters/db2/__init__.py | 1 + .../integration/adapters/db2/adbc/__init__.py | 1 + .../adapters/db2/adbc/test_adbc_db2.py | 46 ++ .../adapters/oracle/adbc/__init__.py | 1 + .../adapters/oracle/adbc/test_adbc_oracle.py | 52 +++ .../adapters/snowflake/__init__.py | 1 + .../adapters/snowflake/adbc/__init__.py | 1 + .../snowflake/adbc/test_adbc_snowflake.py | 51 +++ tests/integration/fixtures/adbc.py | 398 +++++++++++++++++- tests/integration/fixtures/db2.py | 122 +++++- .../test_adbc_config_normalization.py | 185 ++++++-- 14 files changed, 978 insertions(+), 59 deletions(-) create mode 100644 tests/integration/adapters/db2/__init__.py create mode 100644 tests/integration/adapters/db2/adbc/__init__.py create mode 100644 tests/integration/adapters/db2/adbc/test_adbc_db2.py create mode 100644 tests/integration/adapters/oracle/adbc/__init__.py create mode 100644 tests/integration/adapters/oracle/adbc/test_adbc_oracle.py create mode 100644 tests/integration/adapters/snowflake/__init__.py create mode 100644 tests/integration/adapters/snowflake/adbc/__init__.py create mode 100644 tests/integration/adapters/snowflake/adbc/test_adbc_snowflake.py diff --git a/sqlspec/adapters/adbc/core.py b/sqlspec/adapters/adbc/core.py index d88ab09c6..273c6b615 100644 --- a/sqlspec/adapters/adbc/core.py +++ b/sqlspec/adapters/adbc/core.py @@ -469,6 +469,8 @@ def driver_from_uri(uri: str) -> "str | None": for prefix, driver_path in _URI_PREFIX_DRIVER: if lowered.startswith(prefix): return driver_path + if _parse_db2_cli_dsn(uri) is not None: + return "adbc_driver_db2.dbapi.connect" return None @@ -504,7 +506,7 @@ def driver_kind_from_uri(uri: str) -> "str | None": return "quack" if lowered.startswith("oracle://"): return "oracle" - if lowered.startswith("db2://"): + if lowered.startswith("db2://") or _parse_db2_cli_dsn(uri) is not None: return "db2" for prefix, driver_path in _URI_PREFIX_DRIVER: if lowered.startswith(prefix): @@ -627,7 +629,7 @@ def resolve_dialect_from_config(connection_config: "Mapping[str, Any]") -> str: return "mssql" if lowered_uri.startswith("oracle://"): return "oracle" - if lowered_uri.startswith("db2://"): + if lowered_uri.startswith("db2://") or _parse_db2_cli_dsn(uri) is not None: return "db2" if driver_name is None and lowered_uri.startswith("grpc+tls://"): return "duckdb" @@ -713,6 +715,8 @@ def build_connection_config(connection_config: "Mapping[str, Any]") -> "dict[str if isinstance(uri, str) and driver_kind == "duckdb" and uri.lower().startswith("duckdb://"): config["path"] = uri[9:] config.pop("uri", None) + if isinstance(uri, str) and driver_kind == "db2" and not uri.lower().startswith("db2://"): + _normalize_db2_cli_dsn(config, uri) legacy_entrypoint = config.pop("adbc_driver_manager_entrypoint", None) if legacy_entrypoint is not None: @@ -2025,6 +2029,71 @@ def _lift_driver_specific_db_kwargs( config["db_kwargs"] = db_kwargs_dict +def _parse_db2_cli_dsn(uri: str) -> "dict[str, str] | None": + """Parse a semicolon-delimited IBM Db2 CLI keyword DSN when valid.""" + if "://" in uri or ";" not in uri: + return None + parsed: dict[str, str] = {} + for raw_segment in uri.split(";"): + segment = raw_segment.strip() + if not segment or "=" not in segment: + continue + key, value = segment.split("=", 1) + normalized_key = key.strip().lower() + normalized_val = value.strip() + if normalized_key and normalized_val: + parsed[normalized_key] = normalized_val + if "driver" in parsed: + return None + database = parsed.get("database") or parsed.get("dbalias") + host = parsed.get("hostname") or parsed.get("host") + if not database or not host: + return None + return parsed + + +def _normalize_db2_cli_dsn(config: "dict[str, Any]", uri: str) -> None: + """Convert an IBM Db2 CLI keyword DSN into a db2:// URI, credentials, and db_kwargs.""" + parsed = _parse_db2_cli_dsn(uri) + if parsed is None: + return + database = parsed.get("database") or parsed.get("dbalias") or "" + host = parsed.get("hostname") or parsed.get("host") or "" + port = parsed.get("port", "") + formatted_host = f"[{host}]" if ":" in host and not host.startswith("[") else host + config["uri"] = f"db2://{formatted_host}:{port}/{database}" if port else f"db2://{formatted_host}/{database}" + + uid = parsed.get("uid") or parsed.get("user") + if uid is not None: + config.setdefault("username", uid) + pwd = parsed.get("pwd") or parsed.get("password") + if pwd is not None: + config.setdefault("password", pwd) + + existing_db_kwargs = config.get("db_kwargs") + db_kwargs_dict: dict[str, Any] = dict(existing_db_kwargs) if isinstance(existing_db_kwargs, dict) else {} + if "uri" in db_kwargs_dict and db_kwargs_dict.get("uri") == uri: + db_kwargs_dict.pop("uri", None) + + security = parsed.get("security", "").lower() + if security in {"ssl", "tls"}: + db_kwargs_dict.setdefault("adbc.db2.tls", "true") + ssl_cert = parsed.get("sslservercertificate") + if ssl_cert is not None: + db_kwargs_dict.setdefault("adbc.db2.tls.ca_cert", ssl_cert) + current_schema = parsed.get("currentschema") + if current_schema is not None: + db_kwargs_dict.setdefault("adbc.db2.current_schema", current_schema) + connect_timeout = parsed.get("connecttimeout") + if connect_timeout is not None: + db_kwargs_dict.setdefault("adbc.db2.connect_timeout", connect_timeout) + + if db_kwargs_dict: + config["db_kwargs"] = db_kwargs_dict + elif isinstance(existing_db_kwargs, dict) and "uri" in existing_db_kwargs: + config.pop("db_kwargs", None) + + def _apply_driver_manager_config(config: "dict[str, Any]", driver_kind: "str | None", driver_name: Any) -> None: if driver_kind == "mssql": config["driver"] = "mssql" diff --git a/sqlspec/adapters/adbc/driver.py b/sqlspec/adapters/adbc/driver.py index ab470d921..e03ccdfbd 100644 --- a/sqlspec/adapters/adbc/driver.py +++ b/sqlspec/adapters/adbc/driver.py @@ -9,6 +9,8 @@ from time import perf_counter from typing import TYPE_CHECKING, Any, Literal, cast, final +import sqlglot + from sqlspec.adapters.adbc._typing import AdbcCursor, AdbcNativeError, AdbcSessionContext from sqlspec.adapters.adbc.core import ( apply_driver_features, @@ -45,6 +47,8 @@ get_cache_config, register_driver_profile, ) +from sqlspec.core.compiler import SQLProcessor +from sqlspec.core.result import DMLResult from sqlspec.driver import BaseSyncExceptionHandler, SyncDriverAdapterBase, SyncRowStream, validate_savepoint_name from sqlspec.exceptions import DatabaseConnectionError, SQLSpecError from sqlspec.utils.arrow_helpers import arrow_reader_with_deferred_close, arrow_table_to_pylist @@ -56,10 +60,13 @@ if TYPE_CHECKING: from collections.abc import Callable + from sqlglot import exp + from sqlspec.adapters.adbc._typing import AdbcConnection, AdbcRawCursor from sqlspec.builder import QueryBuilder - from sqlspec.core import ArrowResult, Statement, StatementFilter + from sqlspec.core import ArrowResult, SQLResult, Statement, StatementFilter from sqlspec.driver import ExecutionResult + from sqlspec.driver._query_cache import CachedQuery from sqlspec.storage import StorageBridgeJob, StorageDestination, StorageFormat, StorageTelemetry from sqlspec.typing import ArrowReturnFormat, StatementParameters @@ -242,16 +249,28 @@ def dispatch_execute(self, cursor: "AdbcRawCursor", statement: SQL) -> "Executio """ sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) self._ensure_statement_profiling() + use_db2_update = False try: execute_parameters = normalize_postgres_empty_parameters(self._dialect_name, prepared_parameters) - cursor.execute(sql, parameters=execute_parameters) + use_db2_update = ( + self._dialect_name == "db2" + and not statement.returns_rows() + and statement.operation_type != "COMMAND" + and bool(execute_parameters) + ) + if use_db2_update: + self._execute_db2_non_query(cursor, sql, execute_parameters) + else: + cursor.execute(sql, parameters=execute_parameters) except Exception: handle_postgres_rollback(self._dialect_name, cursor, logger) raise - is_select_like = statement.returns_rows() or self._should_force_select(statement, cursor) + is_select_like = ( + False if use_db2_update else (statement.returns_rows() or self._should_force_select(statement, cursor)) + ) if is_select_like: arrow_table = cursor.fetch_arrow_table() @@ -359,7 +378,16 @@ def dispatch_execute_script(self, cursor: "AdbcRawCursor", statement: "SQL") -> total_statements == 1 or any(marker in stmt for marker in ("?", "$", "%", "@", ":")) ): stmt_params = normalize_postgres_empty_parameters(self._dialect_name, prepared_parameters) - cursor.execute(stmt, parameters=stmt_params) + if ( + self._dialect_name == "db2" + and stmt_params + and self._is_db2_non_query_statement( + stmt, parent_statement=statement, total_statements=total_statements + ) + ): + self._execute_db2_non_query(cursor, stmt, stmt_params) + else: + cursor.execute(stmt, parameters=stmt_params) else: cursor.execute(stmt) successful_count += 1 @@ -911,6 +939,38 @@ def _resolve_count_result_rowcount(cursor: "AdbcRawCursor", *, fallback: int) -> return first_value return fallback + def _execute_cache_hit_with_cursor( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" + ) -> "SQLResult": + """Execute a cached query, routing parameterized Db2 non-queries through execute_update.""" + if self._dialect_name == "db2" and not cached.operation_profile.returns_rows and params: + with self.with_cursor(self.connection) as cursor: + wire_sql = self._format_wire_sql(cached.compiled_sql) + self._execute_db2_non_query(cursor, wire_sql, params) + affected_rows = self._resolve_count_result_rowcount(cursor, fallback=resolve_rowcount(cursor)) + return DMLResult(cached.operation_type, affected_rows) + return super()._execute_cache_hit_with_cursor(sql, params, cached) + + def _is_db2_non_query_statement(self, sql: str, *, parent_statement: "SQL", total_statements: int) -> bool: + """Return whether a script statement is a non-row-returning Db2 operation.""" + if total_statements == 1: + return not parent_statement.returns_rows() and parent_statement.operation_type != "COMMAND" + expression: exp.Expression | None = sqlglot.maybe_parse(sql, dialect="db2") + if expression is None: + return False + operation_type = SQLProcessor._operation_type(expression) + if operation_type == "COMMAND": + return False + return not SQLProcessor._operation_profile(expression, operation_type).returns_rows + + @staticmethod + def _execute_db2_non_query(cursor: "AdbcRawCursor", sql: str, execute_parameters: Any) -> None: + """Execute a parameterized Db2 non-query statement via ADBC execute_update.""" + if hasattr(execute_parameters, "__arrow_c_array__") or hasattr(execute_parameters, "__arrow_c_stream__"): + cursor.executemany(sql, execute_parameters) + return + cursor.executemany(sql, [execute_parameters]) + def _parse_adbc_ingest_target( table: str, *, mode: Literal["append", "create", "replace", "create_append"] diff --git a/tests/integration/adapters/_shared/adbc_connection.py b/tests/integration/adapters/_shared/adbc_connection.py index b066877f2..9c301cf3f 100644 --- a/tests/integration/adapters/_shared/adbc_connection.py +++ b/tests/integration/adapters/_shared/adbc_connection.py @@ -5,12 +5,18 @@ BigQuery ADBC gating, and explicit PostgreSQL transaction plumbing. """ +from typing import TYPE_CHECKING + import pytest -from pytest_databases.docker.postgres import PostgresService from sqlspec.adapters.adbc import AdbcConfig +from sqlspec.exceptions import ImproperConfigurationError from tests.integration.fixtures.adbc import xfail_if_driver_missing +if TYPE_CHECKING: + from pytest_databases.docker.bigquery import BigQueryService + from pytest_databases.docker.postgres import PostgresService + @pytest.mark.xdist_group("postgres") @pytest.mark.adbc @@ -58,28 +64,27 @@ def test_sqlite_connection() -> None: assert cur.fetchone() == (1,) -@pytest.mark.skipif( - "not config.getoption('--run-bigquery-tests', default=False)", - reason="BigQuery ADBC tests require --run-bigquery-tests flag and valid GCP credentials", -) +@pytest.mark.bigquery @pytest.mark.xdist_group("bigquery") @pytest.mark.adbc -@xfail_if_driver_missing -def test_bigquery_connection() -> None: - """Test raw ADBC connection creation to BigQuery when explicitly enabled.""" +def test_bigquery_connection(bigquery_service: "BigQueryService") -> None: + """Verify BigQuery ADBC option normalization and Go driver endpoint handling against bigquery_service.""" + pytest.importorskip("adbc_driver_bigquery") + assert bigquery_service.endpoint.startswith("http://") + assert bigquery_service.project + assert bigquery_service.dataset + config = AdbcConfig( connection_config={ - "driver_name": "adbc_driver_bigquery.dbapi.connect", - "project_id": "test-project", - "dataset_id": "test_dataset", + "driver_name": "bigquery", + "project_id": bigquery_service.project, + "dataset_id": bigquery_service.dataset, + "db_kwargs": {"adbc.bigquery.sql.endpoint": bigquery_service.endpoint}, } ) - with config.create_connection() as conn: - assert conn is not None - with conn.cursor() as cur: - cur.execute("SELECT 1 as test_value") - assert cur.fetchone() == (1,) + with pytest.raises(ImproperConfigurationError, match=r"adbc\.bigquery\.sql\.endpoint"), config.create_connection(): + pass @pytest.mark.xdist_group("postgres") diff --git a/tests/integration/adapters/db2/__init__.py b/tests/integration/adapters/db2/__init__.py new file mode 100644 index 000000000..813f9a6f2 --- /dev/null +++ b/tests/integration/adapters/db2/__init__.py @@ -0,0 +1 @@ +"""IBM Db2 integration tests.""" diff --git a/tests/integration/adapters/db2/adbc/__init__.py b/tests/integration/adapters/db2/adbc/__init__.py new file mode 100644 index 000000000..845b3bb7c --- /dev/null +++ b/tests/integration/adapters/db2/adbc/__init__.py @@ -0,0 +1 @@ +"""IBM Db2 ADBC integration tests.""" diff --git a/tests/integration/adapters/db2/adbc/test_adbc_db2.py b/tests/integration/adapters/db2/adbc/test_adbc_db2.py new file mode 100644 index 000000000..a019374d3 --- /dev/null +++ b/tests/integration/adapters/db2/adbc/test_adbc_db2.py @@ -0,0 +1,46 @@ +"""Containerized IBM Db2 ADBC integration tests using pure-Go adbc-driver-db2.""" + +from typing import TYPE_CHECKING, Any + +import pytest + +if TYPE_CHECKING: + from sqlspec.adapters.adbc import AdbcDriver + +pytestmark = [pytest.mark.db2, pytest.mark.adbc, pytest.mark.integration, pytest.mark.xdist_group("db2")] + + +def test_adbc_db2_sysdummy1_scalar_probe(adbc_db2_session: "AdbcDriver") -> None: + """Pure-Go Db2 ADBC executes a scalar probe against SYSIBM.SYSDUMMY1 over DRDA without clidriver.""" + result = adbc_db2_session.execute("SELECT 1 AS v FROM SYSIBM.SYSDUMMY1") + rows = [_lowercase_keys(row) for row in result.get_data()] + assert len(rows) == 1 + assert int(rows[0]["v"]) == 1 + + +def test_adbc_db2_parameterized_crud_and_arrow(adbc_db2_session: "AdbcDriver") -> None: + """Db2 ADBC executes scratch table DDL, positional and named parameter binding, and Arrow fetches.""" + adbc_db2_session.execute("INSERT INTO test_table_adbc_db2 (id, name, amount) VALUES (?, ?, ?)", (1, "alpha", 25)) + adbc_db2_session.execute("INSERT INTO test_table_adbc_db2 (id, name, amount) VALUES (?, ?, ?)", (2, "beta", 50)) + adbc_db2_session.commit() + + filtered = adbc_db2_session.execute( + "SELECT id, name, amount FROM test_table_adbc_db2 WHERE id = :item_id", {"item_id": 1} + ) + filtered_rows = [_lowercase_keys(row) for row in filtered.get_data()] + assert len(filtered_rows) == 1 + assert int(filtered_rows[0]["id"]) == 1 + assert filtered_rows[0]["name"] == "alpha" + assert int(filtered_rows[0]["amount"]) == 25 + + arrow_result = adbc_db2_session.select_to_arrow("SELECT id, name, amount FROM test_table_adbc_db2 ORDER BY id") + assert arrow_result.num_rows == 2 + assert [col.lower() for col in arrow_result.column_names] == ["id", "name", "amount"] + arrow_rows = [_lowercase_keys(row) for row in arrow_result] + assert [row["name"] for row in arrow_rows] == ["alpha", "beta"] + assert [int(row["amount"]) for row in arrow_rows] == [25, 50] + + +def _lowercase_keys(row: dict[str, Any]) -> dict[str, Any]: + """Normalize uppercase database column identifiers to lowercase keys.""" + return {key.lower(): value for key, value in row.items()} diff --git a/tests/integration/adapters/oracle/adbc/__init__.py b/tests/integration/adapters/oracle/adbc/__init__.py new file mode 100644 index 000000000..5e37be848 --- /dev/null +++ b/tests/integration/adapters/oracle/adbc/__init__.py @@ -0,0 +1 @@ +"""Oracle ADBC integration tests.""" diff --git a/tests/integration/adapters/oracle/adbc/test_adbc_oracle.py b/tests/integration/adapters/oracle/adbc/test_adbc_oracle.py new file mode 100644 index 000000000..f03511adf --- /dev/null +++ b/tests/integration/adapters/oracle/adbc/test_adbc_oracle.py @@ -0,0 +1,52 @@ +"""Containerized Oracle Database ADBC integration tests using pure-Go adbc-driver-oracle.""" + +from typing import TYPE_CHECKING, Any + +import pytest + +if TYPE_CHECKING: + from sqlspec.adapters.adbc import AdbcDriver + +pytestmark = [pytest.mark.oracle, pytest.mark.adbc, pytest.mark.integration, pytest.mark.xdist_group("oracle")] + + +def test_adbc_oracle_dual_scalar_probe(adbc_oracle_session: "AdbcDriver") -> None: + """Pure-Go Oracle ADBC executes a scalar probe against DUAL without Oracle Instant Client.""" + result = adbc_oracle_session.execute("SELECT 1 AS v FROM DUAL") + rows = [_lowercase_keys(row) for row in result.get_data()] + assert len(rows) == 1 + assert int(rows[0]["v"]) == 1 + + +def test_adbc_oracle_parameterized_crud_and_arrow(adbc_oracle_session: "AdbcDriver") -> None: + """Oracle ADBC executes scratch table DDL, positional and named parameter binding, and Arrow fetches.""" + adbc_oracle_session.execute( + "INSERT INTO test_table_adbc_oracle (id, name, amount) VALUES (:1, :2, :3)", (1, "alpha", 15) + ) + adbc_oracle_session.execute( + "INSERT INTO test_table_adbc_oracle (id, name, amount) VALUES (:1, :2, :3)", (2, "beta", 30) + ) + adbc_oracle_session.commit() + + filtered = adbc_oracle_session.execute( + "SELECT id, name, amount FROM test_table_adbc_oracle WHERE id = :item_id", {"item_id": 2} + ) + filtered_rows = [_lowercase_keys(row) for row in filtered.get_data()] + assert len(filtered_rows) == 1 + assert int(filtered_rows[0]["id"]) == 2 + assert filtered_rows[0]["name"] == "beta" + assert int(filtered_rows[0]["amount"]) == 30 + + arrow_result = adbc_oracle_session.select_to_arrow( + "SELECT id, name, amount FROM test_table_adbc_oracle ORDER BY id" + ) + assert arrow_result.num_rows == 2 + assert [col.lower() for col in arrow_result.column_names] == ["id", "name", "amount"] + arrow_rows = [_lowercase_keys(row) for row in arrow_result] + assert [row["name"] for row in arrow_rows] == ["alpha", "beta"] + assert [int(row["amount"]) for row in arrow_rows] == [15, 30] + + +def _lowercase_keys(row: dict[str, Any]) -> dict[str, Any]: + """Normalize uppercase database column identifiers to lowercase keys.""" + return {key.lower(): value for key, value in row.items()} diff --git a/tests/integration/adapters/snowflake/__init__.py b/tests/integration/adapters/snowflake/__init__.py new file mode 100644 index 000000000..6ef5ac141 --- /dev/null +++ b/tests/integration/adapters/snowflake/__init__.py @@ -0,0 +1 @@ +"""Snowflake integration tests.""" diff --git a/tests/integration/adapters/snowflake/adbc/__init__.py b/tests/integration/adapters/snowflake/adbc/__init__.py new file mode 100644 index 000000000..80bd09a25 --- /dev/null +++ b/tests/integration/adapters/snowflake/adbc/__init__.py @@ -0,0 +1 @@ +"""Snowflake ADBC integration tests.""" diff --git a/tests/integration/adapters/snowflake/adbc/test_adbc_snowflake.py b/tests/integration/adapters/snowflake/adbc/test_adbc_snowflake.py new file mode 100644 index 000000000..488afaf49 --- /dev/null +++ b/tests/integration/adapters/snowflake/adbc/test_adbc_snowflake.py @@ -0,0 +1,51 @@ +"""Containerized Snowflake ADBC integration tests against Embucket using adbc-driver-snowflake.""" + +from typing import TYPE_CHECKING, Any + +import pytest + +if TYPE_CHECKING: + from sqlspec.adapters.adbc import AdbcDriver + +pytestmark = [pytest.mark.snowflake, pytest.mark.adbc, pytest.mark.integration, pytest.mark.xdist_group("snowflake")] + + +def test_adbc_snowflake_scalar_probe(adbc_snowflake_session: "AdbcDriver") -> None: + """Snowflake ADBC executes a scalar SELECT probe over HTTP against Embucket.""" + result = adbc_snowflake_session.execute("SELECT 1 AS v") + rows = [_lowercase_keys(row) for row in result.get_data()] + assert len(rows) == 1 + assert int(rows[0]["v"]) == 1 + + +def test_adbc_snowflake_parameterized_crud_and_arrow(adbc_snowflake_session: "AdbcDriver") -> None: + """Snowflake ADBC executes scratch table DDL, parameter binding, and Arrow fetches against Embucket.""" + adbc_snowflake_session.execute( + "INSERT INTO test_table_adbc_snowflake (id, name, amount) VALUES (?, ?, ?)", (1, "alpha", 40) + ) + adbc_snowflake_session.execute( + "INSERT INTO test_table_adbc_snowflake (id, name, amount) VALUES (?, ?, ?)", (2, "beta", 80) + ) + + filtered = adbc_snowflake_session.execute( + "SELECT id, name, amount FROM test_table_adbc_snowflake WHERE id = ?", (2,) + ) + filtered_rows = [_lowercase_keys(row) for row in filtered.get_data()] + assert len(filtered_rows) == 1 + assert int(filtered_rows[0]["id"]) == 2 + assert filtered_rows[0]["name"] == "beta" + assert int(filtered_rows[0]["amount"]) == 80 + + arrow_result = adbc_snowflake_session.select_to_arrow( + "SELECT id, name, amount FROM test_table_adbc_snowflake ORDER BY id" + ) + assert arrow_result.num_rows == 2 + assert [col.lower() for col in arrow_result.column_names] == ["id", "name", "amount"] + arrow_rows = [_lowercase_keys(row) for row in arrow_result] + assert [row["name"] for row in arrow_rows] == ["alpha", "beta"] + assert [int(row["amount"]) for row in arrow_rows] == [40, 80] + + +def _lowercase_keys(row: dict[str, Any]) -> dict[str, Any]: + """Normalize uppercase database column identifiers to lowercase keys.""" + return {key.lower(): value for key, value in row.items()} diff --git a/tests/integration/fixtures/adbc.py b/tests/integration/fixtures/adbc.py index 0d9b54c31..196393eb5 100644 --- a/tests/integration/fixtures/adbc.py +++ b/tests/integration/fixtures/adbc.py @@ -1,18 +1,33 @@ """Test fixtures and configuration for ADBC integration tests.""" +import contextlib import functools +import gzip +import http.server +import threading +import urllib.error +import urllib.request from collections.abc import Callable, Generator from importlib.util import find_spec -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast import pytest +from pytest_databases.docker.snowflake import SNOWFLAKE_HTTP_PORT, SnowflakeService, check_snowflake_health +from pytest_databases.helpers import get_xdist_worker_num from sqlspec.adapters.adbc import AdbcConfig, AdbcDriver +from sqlspec.utils.serializers import from_json, to_json if TYPE_CHECKING: + from pytest_databases import ContainerService + from pytest_databases.docker.db2 import Db2Service from pytest_databases.docker.gizmosql import GizmoSQLService + from pytest_databases.docker.oracle import OracleService + from pytest_databases.types import ServiceContainer, XdistIsolationLevel __all__ = ( + "adbc_db2_config", + "adbc_db2_session", "adbc_duckdb_config", "adbc_duckdb_driver", "adbc_duckdb_session", @@ -22,10 +37,15 @@ "adbc_gizmosql_sqlite_config", "adbc_gizmosql_sqlite_connection_config", "adbc_gizmosql_sqlite_session", + "adbc_oracle_config", + "adbc_oracle_session", "adbc_postgresql_config", "adbc_postgresql_session", + "adbc_snowflake_config", + "adbc_snowflake_session", "adbc_sqlite_config", "adbc_sqlite_session", + "snowflake_service", "xfail_if_driver_missing", ) @@ -210,6 +230,382 @@ def adbc_duckdb_driver(adbc_duckdb_session: AdbcDriver) -> Generator[AdbcDriver, yield adbc_duckdb_session +@pytest.fixture(scope="session") +def adbc_oracle_config(oracle_23ai_service: "OracleService") -> Generator[AdbcConfig, None, None]: + """Provide an ADBC configuration connected to Oracle 23ai via pure-Go adbc-driver-oracle.""" + pytest.importorskip("adbc_driver_oracle") + config = AdbcConfig( + connection_config={ + "driver_name": "oracle", + "uri": f"{oracle_23ai_service.host}:{oracle_23ai_service.port}/{oracle_23ai_service.service_name}", + "username": oracle_23ai_service.user, + "password": oracle_23ai_service.password, + } + ) + try: + yield config + finally: + config.close_pool() + + +@pytest.fixture(scope="function") +def adbc_oracle_session(adbc_oracle_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: + """Yield an Oracle ADBC session with a clean scratch table.""" + with adbc_oracle_config.provide_session() as session: + _drop_table_quietly(session, "DROP TABLE test_table_adbc_oracle PURGE") + session.execute( + """ + CREATE TABLE test_table_adbc_oracle ( + id NUMBER(10) PRIMARY KEY, + name VARCHAR2(100) NOT NULL, + amount NUMBER(10) NOT NULL + ) + """ + ) + session.commit() + try: + yield session + finally: + _drop_table_quietly(session, "DROP TABLE test_table_adbc_oracle PURGE") + + +@pytest.fixture(scope="session") +def adbc_db2_config(db2_service: "Db2Service") -> Generator[AdbcConfig, None, None]: + """Provide an ADBC configuration connected to IBM Db2 via pure-Go adbc-driver-db2.""" + pytest.importorskip("adbc_driver_db2") + config = AdbcConfig(connection_config={"driver_name": "db2", "uri": db2_service.dsn}) + try: + yield config + finally: + config.close_pool() + + +@pytest.fixture(scope="function") +def adbc_db2_session(adbc_db2_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: + """Yield an IBM Db2 ADBC session with a clean scratch table.""" + with adbc_db2_config.provide_session() as session: + _drop_table_quietly(session, "DROP TABLE test_table_adbc_db2") + session.execute( + """ + CREATE TABLE test_table_adbc_db2 ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + amount INTEGER NOT NULL + ) + """ + ) + session.commit() + try: + yield session + finally: + _drop_table_quietly(session, "DROP TABLE test_table_adbc_db2") + + +@pytest.fixture(scope="session") +def snowflake_service( + container_service: "ContainerService", + snowflake_image: str, + snowflake_user: str, + snowflake_password: str, + snowflake_account: str, + snowflake_database: str, + snowflake_schema: str, + snowflake_warehouse: str, + snowflake_role: str, + xdist_snowflake_isolation_level: "XdistIsolationLevel", +) -> Generator[SnowflakeService, None, None]: + """Start Embucket with a Snowflake REST wire-compatibility proxy for gosnowflake ADBC.""" + worker_num = get_xdist_worker_num() + container_name = ( + f"snowflake_{worker_num}" + if worker_num is not None and xdist_snowflake_isolation_level == "server" + else "snowflake" + ) + transient = worker_num is not None and xdist_snowflake_isolation_level == "server" + with ( + container_service.run( + image=snowflake_image, + name=container_name, + container_port=SNOWFLAKE_HTTP_PORT, + env={"EMBUCKET_PORT": str(SNOWFLAKE_HTTP_PORT), "NO_COLOR": "true"}, + check=_is_embucket_ready, + timeout=90, + pause=0.5, + transient=transient, + ) as service, + _provide_embucket_snowflake_proxy(service.host, service.port) as proxy_port, + ): + yield SnowflakeService( + container=service.container, + host="127.0.0.1", + port=proxy_port, + user=snowflake_user, + password=snowflake_password, + account=snowflake_account, + database=snowflake_database, + schema=snowflake_schema, + warehouse=snowflake_warehouse, + role=snowflake_role, + ) + + +@pytest.fixture(scope="session") +def adbc_snowflake_config(snowflake_service: SnowflakeService) -> Generator[AdbcConfig, None, None]: + """Provide an ADBC configuration connected to Embucket Snowflake emulator via adbc-driver-snowflake.""" + pytest.importorskip("adbc_driver_snowflake") + config = AdbcConfig( + connection_config={ + "driver_name": "snowflake", + "db_kwargs": { + "username": snowflake_service.user, + "password": snowflake_service.password, + "adbc.snowflake.sql.account": snowflake_service.account, + "adbc.snowflake.sql.db": snowflake_service.database, + "adbc.snowflake.sql.schema": snowflake_service.schema, + "adbc.snowflake.sql.warehouse": snowflake_service.warehouse, + "adbc.snowflake.sql.role": snowflake_service.role, + "adbc.snowflake.sql.uri.protocol": "http", + "adbc.snowflake.sql.uri.host": snowflake_service.host, + "adbc.snowflake.sql.uri.port": str(snowflake_service.port), + }, + } + ) + try: + with config.provide_session() as bootstrap_session: + bootstrap_session.execute( + "CREATE EXTERNAL VOLUME IF NOT EXISTS embucket_mem_vol " + "STORAGE_LOCATIONS = ((NAME = 'mem' STORAGE_PROVIDER = 'MEMORY'))" + ) + bootstrap_session.execute( + f"CREATE DATABASE IF NOT EXISTS {snowflake_service.database} EXTERNAL_VOLUME = 'embucket_mem_vol'" + ) + bootstrap_session.execute( + f"CREATE SCHEMA IF NOT EXISTS {snowflake_service.database}.{snowflake_service.schema}" + ) + yield config + finally: + config.close_pool() + + +@pytest.fixture(scope="function") +def adbc_snowflake_session(adbc_snowflake_config: AdbcConfig) -> Generator[AdbcDriver, None, None]: + """Yield a Snowflake ADBC session with a clean scratch table.""" + with adbc_snowflake_config.provide_session() as session: + _drop_table_quietly(session, "DROP TABLE IF EXISTS test_table_adbc_snowflake") + session.execute( + """ + CREATE TABLE test_table_adbc_snowflake ( + id INTEGER, + name VARCHAR, + amount INTEGER + ) + """ + ) + try: + yield session + finally: + _drop_table_quietly(session, "DROP TABLE IF EXISTS test_table_adbc_snowflake") + + +class _EmbucketSnowflakeProxyHandler(http.server.BaseHTTPRequestHandler): + """Translate Snowflake REST v1 qmark bindings and rowset cell types for Embucket.""" + + upstream_host: ClassVar[str] = "127.0.0.1" + upstream_port: ClassVar[int] = SNOWFLAKE_HTTP_PORT + + def log_message(self, format: str, *args: object) -> None: + """Silence per-request HTTP proxy stderr logging.""" + _ = (format, args) + + def do_GET(self) -> None: + """Forward HTTP GET requests to Embucket.""" + self._forward("GET") + + def do_POST(self) -> None: + """Forward HTTP POST requests to Embucket with Snowflake REST normalization.""" + self._forward("POST") + + def do_DELETE(self) -> None: + """Forward HTTP DELETE requests to Embucket.""" + self._forward("DELETE") + + def _forward(self, method: str) -> None: + """Forward an HTTP request to Embucket and normalize query-request payloads.""" + content_length = int(self.headers.get("Content-Length", "0")) + request_body = self.rfile.read(content_length) if content_length > 0 else b"" + headers = { + key: value + for key, value in self.headers.items() + if key.lower() not in {"host", "accept-encoding", "content-length", "transfer-encoding", "connection"} + } + if self.headers.get("Content-Encoding", "").lower() == "gzip" and request_body: + request_body = gzip.decompress(request_body) + headers.pop("Content-Encoding", None) + headers.pop("content-encoding", None) + + if method == "POST" and "/queries/v1/query-request" in self.path and request_body: + request_body = self._rewrite_query_request(request_body) + + upstream_url = f"http://{self.upstream_host}:{self.upstream_port}{self.path}" + upstream_request = urllib.request.Request( + upstream_url, data=request_body if method == "POST" else None, headers=headers, method=method + ) + try: + with urllib.request.urlopen(upstream_request, timeout=30) as response: + status = response.status + response_body = response.read() + response_headers = list(response.headers.items()) + except urllib.error.HTTPError as exc: + status = exc.code + response_body = exc.read() + response_headers = list(exc.headers.items()) + + if response_body: + response_body = self._normalize_response(response_body) + + self.send_response(status) + for header_name, header_value in response_headers: + if header_name.lower() not in {"content-length", "transfer-encoding", "content-encoding", "connection"}: + self.send_header(header_name, header_value) + self.send_header("Content-Length", str(len(response_body))) + self.end_headers() + if response_body: + self.wfile.write(response_body) + + @classmethod + def _rewrite_query_request(cls, body: bytes) -> bytes: + """Substitute Snowflake REST bindings into sqlText for Embucket's DataFusion planner.""" + try: + payload = from_json(body) + except Exception: + return body + if not isinstance(payload, dict): + return body + sql_text = payload.get("sqlText") + bindings = payload.get("bindings") + if not isinstance(sql_text, str) or not isinstance(bindings, dict) or not bindings: + return body + payload["sqlText"] = cls._substitute_qmark_bindings(sql_text, bindings) + payload.pop("bindings", None) + return to_json(payload, as_bytes=True) + + @classmethod + def _substitute_qmark_bindings(cls, sql_text: str, bindings: dict[str, Any]) -> str: + """Replace unquoted qmark placeholders with typed Snowflake REST binding literals.""" + output: list[str] = [] + index = 0 + length = len(sql_text) + binding_position = 1 + in_single_quote = False + in_double_quote = False + + while index < length: + char = sql_text[index] + if char == "'" and not in_double_quote: + output.append(char) + if in_single_quote and index + 1 < length and sql_text[index + 1] == "'": + output.append("'") + index += 2 + continue + in_single_quote = not in_single_quote + index += 1 + continue + if char == '"' and not in_single_quote: + output.append(char) + if in_double_quote and index + 1 < length and sql_text[index + 1] == '"': + output.append('"') + index += 2 + continue + in_double_quote = not in_double_quote + index += 1 + continue + if char == "?" and not in_single_quote and not in_double_quote: + binding_entry = bindings.get(str(binding_position)) + binding_position += 1 + output.append(cls._format_binding_literal(binding_entry)) + index += 1 + continue + output.append(char) + index += 1 + + return "".join(output) + + @staticmethod + def _format_binding_literal(binding_entry: Any) -> str: + """Format a single Snowflake REST binding entry into a SQL literal.""" + if not isinstance(binding_entry, dict): + return "NULL" + raw_value = binding_entry.get("value") + if raw_value is None: + return "NULL" + binding_type = str(binding_entry.get("type", "TEXT")).upper() + text_value = str(raw_value) + if binding_type in {"FIXED", "REAL"}: + return text_value + if binding_type == "BOOLEAN": + return "TRUE" if text_value.lower() in {"true", "1"} else "FALSE" + escaped = text_value.replace("'", "''") + return f"'{escaped}'" + + @staticmethod + def _normalize_response(body: bytes) -> bytes: + """Coerce non-null Embucket JSON rowset cells to strings matching Snowflake REST v1.""" + try: + payload = from_json(body) + except Exception: + return body + if not isinstance(payload, dict): + return body + data = payload.get("data") + if not isinstance(data, dict): + return body + rowset = data.get("rowset") + if not isinstance(rowset, list): + return body + data["rowset"] = [ + [None if cell is None else str(cell) for cell in row] if isinstance(row, list) else row for row in rowset + ] + return to_json(payload, as_bytes=True) + + +def _is_embucket_ready(service: "ServiceContainer") -> bool: + """Return True when the Embucket HTTP health endpoint responds with HTTP 200.""" + return check_snowflake_health(service.host, service.port) + + +@contextlib.contextmanager +def _provide_embucket_snowflake_proxy(upstream_host: str, upstream_port: int) -> Generator[int, None, None]: + """Run a background HTTP forwarder normalizing Snowflake REST payloads for Embucket.""" + + class _BoundProxyHandler(_EmbucketSnowflakeProxyHandler): + pass + + _BoundProxyHandler.upstream_host = upstream_host + _BoundProxyHandler.upstream_port = upstream_port + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _BoundProxyHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield int(server.server_address[1]) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +def _drop_table_quietly(session: AdbcDriver, drop_sql: str) -> None: + """Best-effort table teardown with transaction recovery.""" + with contextlib.suppress(Exception): + session.rollback() + try: + session.execute(drop_sql) + with contextlib.suppress(Exception): + session.commit() + except Exception: + with contextlib.suppress(Exception): + session.rollback() + + def _require_gizmosql_driver() -> None: """Skip when neither GizmoSQL nor FlightSQL ADBC driver package is installed.""" if find_spec("adbc_driver_gizmosql") is None and find_spec("adbc_driver_flightsql") is None: diff --git a/tests/integration/fixtures/db2.py b/tests/integration/fixtures/db2.py index 8a6a86fea..01f62eae8 100644 --- a/tests/integration/fixtures/db2.py +++ b/tests/integration/fixtures/db2.py @@ -1,15 +1,25 @@ """IBM Db2 integration test fixtures.""" import os +import shlex from collections.abc import AsyncGenerator, Generator -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pytest +from pytest_databases.docker.db2 import ( + DB2_PORT, + Db2Service, + create_worker_database, + exec_db2_sql, + validate_db2_database_name, +) +from pytest_databases.helpers import exec_output_to_bytes, get_xdist_worker_num from sqlspec.adapters.db2.config import Db2AsyncConfig, Db2ConnectionParams, Db2SyncConfig if TYPE_CHECKING: - from pytest_databases.docker.db2 import Db2Service + from pytest_databases import ContainerService + from pytest_databases.types import ServiceContainer, XdistIsolationLevel from sqlspec.adapters.db2.driver import Db2AsyncDriver, Db2SyncDriver @@ -18,6 +28,7 @@ "db2_async_session", "db2_connection_config", "db2_image", + "db2_service", "db2_session", "db2_sync_config", ) @@ -30,7 +41,95 @@ def db2_image() -> str: @pytest.fixture(scope="session") -def db2_connection_config(db2_service: "Db2Service") -> Db2ConnectionParams: +def db2_service( + container_service: "ContainerService", + db2_image: str, + db2_dbname: str, + db2_username: str, + db2_password: str, + db2_startup_timeout: int, + xdist_db2_isolation_level: "XdistIsolationLevel", +) -> Generator[Db2Service, None, None]: + """Start and yield a ready IBM Db2 service container with automatic 4Kn Direct-IO recovery.""" + worker_num = get_xdist_worker_num() + name = "db2" + initial_db = ( + validate_db2_database_name(os.getenv("DB2_DBNAME", "pytestdb")) + if worker_num is not None and xdist_db2_isolation_level == "database" + else db2_dbname + ) + if worker_num is not None and xdist_db2_isolation_level == "server": + name = f"db2_{worker_num + 1}" + + fallback_attempted = False + + def check(service: "ServiceContainer") -> bool: + nonlocal fallback_attempted + needs_4k_fallback = False + output = "" + try: + output = exec_db2_sql( + service.container, "SELECT 1 FROM SYSIBM.SYSDUMMY1", db_name=initial_db, instance_user=db2_username + ) + except RuntimeError as exc: + error_text = str(exc) + if not fallback_attempted and ("SQL1013N" in error_text or "42705" in error_text): + needs_4k_fallback = True + else: + return False + + if needs_4k_fallback: + fallback_attempted = True + _ensure_db2_4k_database(service.container, initial_db, instance_user=db2_username) + return False + + return "1" in output + + with container_service.run( + image=db2_image, + container_port=DB2_PORT, + name=name, + entrypoint=["/bin/bash", "-c"], + command=[ + ( + "sed -i 's/db2set DB2COMM=TCPIP/" + "db2set DB2COMM=TCPIP \\&\\& db2set DB2_DIRECT_IO=NO \\&\\& db2set DB2_4K_DEVICE_SUPPORT=ON/' " + "/var/db2_setup/include/db2_common_functions && " + "exec /var/db2_setup/lib/setup_db2_instance.sh" + ) + ], + env={ + "LICENSE": "accept", + "DB2INST1_PASSWORD": db2_password, + "DBNAME": initial_db, + "SAMPLEDB": "false", + "AUTOCONFIG": "false", + "ARCHIVE_LOGS": "false", + "TO_CREATE_SAMPLEDB": "false", + "REPODB": "false", + }, + wait_for_log="Setup has completed", + check=check, + timeout=db2_startup_timeout, + pause=2.0, + transient=xdist_db2_isolation_level == "server", + privileged=True, + ipc_mode="host", + ) as service: + if db2_dbname != initial_db: + create_worker_database(service.container, db2_dbname, instance_user=db2_username) + yield Db2Service( + container=service.container, + host=service.host, + port=service.port, + db_name=db2_dbname, + username=db2_username, + password=db2_password, + ) + + +@pytest.fixture(scope="session") +def db2_connection_config(db2_service: Db2Service) -> Db2ConnectionParams: """Build SQLSpec Db2 connection parameters from the Db2 service container.""" return { "database": db2_service.db_name, @@ -74,3 +173,20 @@ async def db2_async_session(db2_async_config: Db2AsyncConfig) -> "AsyncGenerator """Yield a function-scoped Db2 asynchronous driver session.""" async with db2_async_config.provide_session() as driver: yield driver + + +def _ensure_db2_4k_database(container: Any, db_name: str, *, instance_user: str) -> None: + """Configure 4Kn Direct-IO registry settings and create the initial Db2 database if missing.""" + validate_db2_database_name(db_name) + quoted_db = shlex.quote(db_name) + inner_script = ( + f"db2 list db directory | grep -iq 'Database name.*= {db_name}' || " + f"(db2set DB2_DIRECT_IO=NO && db2set DB2_4K_DEVICE_SUPPORT=ON && " + f"db2stop force && db2start && db2 create database {quoted_db})" + ) + script = f"flock /tmp/sqlspec_db2_init.lock -c {shlex.quote(inner_script)}" + result = container.exec_run(["su", "-", instance_user, "-c", script]) + if result.exit_code != 0: + decoded = exec_output_to_bytes(result.output).decode("utf-8", errors="replace") + msg = f"Failed to initialize 4Kn Db2 database {db_name!r} (exit {result.exit_code}): {decoded.strip()}" + raise RuntimeError(msg) diff --git a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py index 2f9c57ae8..fb91359c7 100644 --- a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py +++ b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py @@ -3,6 +3,7 @@ from typing import Any, get_type_hints from unittest.mock import MagicMock +import adbc_driver_manager.dbapi import pytest from adbc_driver_bigquery import DatabaseOptions @@ -13,20 +14,10 @@ resolve_driver_connect_func, resolve_driver_name_from_config, ) -from sqlspec.core import ParameterStyle +from sqlspec.core import SQL, ParameterStyle from sqlspec.exceptions import SQLSpecError -def _resolve_driver_name(config: AdbcConfig) -> str: - """Resolve driver name from configuration.""" - return resolve_driver_name_from_config(config.connection_config) - - -def _get_connection_config_dict(config: AdbcConfig) -> dict[str, Any]: - """Build the normalized connection configuration.""" - return build_connection_config(config.connection_config) - - def test_adbc_config_runs_connection_create_callback(monkeypatch: Any) -> None: """ADBC should run the connection hook when it creates a physical connection.""" connection = MagicMock() @@ -82,14 +73,16 @@ def test_resolve_driver_name_from_uri() -> None: assert _resolve_driver_name(config) == "adbc_driver_postgresql.dbapi.connect" -def test_resolve_driver_name_gizmosql_alias() -> None: - """Resolve GizmoSQL aliases to the FlightSQL driver.""" +def test_resolve_driver_name_gizmosql_alias(monkeypatch: Any) -> None: + """Resolve GizmoSQL aliases to the FlightSQL driver when native gizmosql driver is absent.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmosql"}) assert _resolve_driver_name(config) == "adbc_driver_flightsql.dbapi.connect" -def test_resolve_driver_name_from_gizmosql_uri() -> None: - """Detect FlightSQL driver from GizmoSQL URI schemes.""" +def test_resolve_driver_name_from_gizmosql_uri(monkeypatch: Any) -> None: + """Detect FlightSQL driver from GizmoSQL URI schemes when native gizmosql driver is absent.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"uri": "gizmosql://localhost:31337"}) assert _resolve_driver_name(config) == "adbc_driver_flightsql.dbapi.connect" config = AdbcConfig(connection_config={"uri": "gizmo://localhost:31337"}) @@ -160,8 +153,9 @@ def test_connection_config_dict_preserves_db_kwargs_for_non_bigquery() -> None: assert "foo" not in resolved -def test_flightsql_lifts_username_password_into_db_kwargs() -> None: - """Top-level username/password move into db_kwargs for GizmoSQL.""" +def test_flightsql_lifts_username_password_into_db_kwargs(monkeypatch: Any) -> None: + """Top-level username/password move into db_kwargs on the FlightSQL fallback route.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmosql", "username": "alice", "password": "secret"}) resolved = _get_connection_config_dict(config) @@ -170,32 +164,36 @@ def test_flightsql_lifts_username_password_into_db_kwargs() -> None: assert "password" not in resolved -def test_flightsql_lifts_tls_skip_verify_true() -> None: - """tls_skip_verify=True maps to the FlightSQL ADBC option key.""" +def test_flightsql_lifts_tls_skip_verify_true(monkeypatch: Any) -> None: + """tls_skip_verify=True maps to the FlightSQL ADBC option key on the FlightSQL fallback route.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmosql", "tls_skip_verify": True}) resolved = _get_connection_config_dict(config) assert resolved["db_kwargs"]["adbc.flight.sql.client_option.tls_skip_verify"] == "true" -def test_flightsql_lifts_tls_skip_verify_false() -> None: - """tls_skip_verify=False maps to the FlightSQL ADBC option key.""" +def test_flightsql_lifts_tls_skip_verify_false(monkeypatch: Any) -> None: + """tls_skip_verify=False maps to the FlightSQL ADBC option key on the FlightSQL fallback route.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmosql", "tls_skip_verify": False}) resolved = _get_connection_config_dict(config) assert resolved["db_kwargs"]["adbc.flight.sql.client_option.tls_skip_verify"] == "false" -def test_flightsql_lifts_authorization_header() -> None: +def test_flightsql_lifts_authorization_header(monkeypatch: Any) -> None: """authorization_header maps to the FlightSQL ADBC option key.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmosql", "authorization_header": "Bearer token"}) resolved = _get_connection_config_dict(config) assert resolved["db_kwargs"]["adbc.flight.sql.authorization_header"] == "Bearer token" -def test_flightsql_explicit_db_kwargs_wins_over_top_level_shortcuts() -> None: +def test_flightsql_explicit_db_kwargs_wins_over_top_level_shortcuts(monkeypatch: Any) -> None: """User-supplied db_kwargs entries take precedence over top-level shortcuts.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig( connection_config={ "driver_name": "gizmosql", @@ -284,14 +282,13 @@ def test_grpc_tls_uri_defaults_to_duckdb() -> None: def test_gizmosql_parameter_style_is_qmark() -> None: """GizmoSQL connections should use qmark parameter style like DuckDB.""" - from sqlspec.core import ParameterStyle - config = AdbcConfig(connection_config={"driver_name": "gizmosql"}) assert config.statement_config.parameter_config.default_parameter_style == ParameterStyle.QMARK -def test_gizmo_alias_resolves_to_flightsql() -> None: - """The short 'gizmo' alias should also resolve to FlightSQL driver.""" +def test_gizmo_alias_resolves_to_flightsql(monkeypatch: Any) -> None: + """The short 'gizmo' alias resolves to FlightSQL driver when native gizmosql driver is absent.""" + monkeypatch.setattr("sqlspec.adapters.adbc.core.import_optional", lambda _name: None) config = AdbcConfig(connection_config={"driver_name": "gizmo"}) assert _resolve_driver_name(config) == "adbc_driver_flightsql.dbapi.connect" assert config.statement_config.dialect == "duckdb" @@ -316,7 +313,6 @@ def test_gizmosql_tls_skip_verify_in_config() -> None: config = AdbcConfig( connection_config={"driver_name": "gizmosql", "uri": "grpc+tls://localhost:31337", "tls_skip_verify": True} ) - # Verify the config accepts tls_skip_verify without error assert config.connection_config.get("tls_skip_verify") is True @@ -342,10 +338,7 @@ def test_gizmosql_backend_duckdb_explicit() -> None: def test_gizmosql_supported_parameter_styles() -> None: """GizmoSQL should support qmark and numeric parameter styles.""" - from sqlspec.core import ParameterStyle - config = AdbcConfig(connection_config={"driver_name": "gizmosql"}) - # GizmoSQL (DuckDB backend) should support multiple styles supported = config.statement_config.parameter_config.supported_parameter_styles assert ParameterStyle.QMARK in supported @@ -375,8 +368,6 @@ def test_shared_object_driver_routes_through_the_driver_manager() -> None: def test_shared_object_driver_connect_func_is_the_driver_manager() -> None: """Resolving the connect function must not try to import the path as a module.""" - import adbc_driver_manager.dbapi - connect_func = resolve_driver_connect_func("/opt/lib/libadbc_driver_postgresql.dylib", None) assert connect_func is adbc_driver_manager.dbapi.connect @@ -663,3 +654,131 @@ def test_quack_driver_aliases_uri_and_token_normalization() -> None: resolved = _get_connection_config_dict(config) assert "token" not in resolved assert resolved["db_kwargs"]["adbc.quack.token"] == "secret-jwt" + + +def test_db2_cli_dsn_normalizes_to_db2_uri_and_credentials() -> None: + """IBM Db2 CLI keyword DSNs normalize to db2:// URIs, top-level credentials, and ADBC db_kwargs.""" + explicit_driver_cfg = AdbcConfig( + connection_config={ + "driver_name": "db2", + "uri": "DATABASE=testdb;HOSTNAME=localhost;PORT=50000;PROTOCOL=TCPIP;UID=db2inst1;PWD=secret;", + } + ) + resolved_explicit = _get_connection_config_dict(explicit_driver_cfg) + assert resolved_explicit == {"uri": "db2://localhost:50000/testdb", "username": "db2inst1", "password": "secret"} + + inferred_dsn_cfg = AdbcConfig( + connection_config={ + "dsn": ( + "DATABASE=testdb;HOSTNAME=127.0.0.1;PORT=50000;UID=db2inst1;PWD=secret;" + "SECURITY=SSL;SSLSERVERCERTIFICATE=/etc/ssl/ca.pem;CURRENTSCHEMA=APP;CONNECTTIMEOUT=10;" + ) + } + ) + assert _resolve_driver_name(inferred_dsn_cfg) == "adbc_driver_db2.dbapi.connect" + assert inferred_dsn_cfg.statement_config.dialect == "db2" + assert inferred_dsn_cfg.statement_config.parameter_config.default_parameter_style == ParameterStyle.QMARK + resolved_inferred = _get_connection_config_dict(inferred_dsn_cfg) + assert resolved_inferred["uri"] == "db2://127.0.0.1:50000/testdb" + assert resolved_inferred["username"] == "db2inst1" + assert resolved_inferred["password"] == "secret" + assert resolved_inferred["db_kwargs"] == { + "adbc.db2.tls": "true", + "adbc.db2.tls.ca_cert": "/etc/ssl/ca.pem", + "adbc.db2.current_schema": "APP", + "adbc.db2.connect_timeout": "10", + } + + override_resolved = build_connection_config({ + "dsn": "DATABASE=testdb;HOSTNAME=db2.internal;UID=dsn_user;PWD=dsn_pwd;CURRENTSCHEMA=DSN_SCHEMA;", + "username": "explicit_user", + "db_kwargs": {"adbc.db2.current_schema": "EXPLICIT_SCHEMA"}, + }) + assert override_resolved["uri"] == "db2://db2.internal/testdb" + assert override_resolved["username"] == "explicit_user" + assert override_resolved["password"] == "dsn_pwd" + assert override_resolved["db_kwargs"] == {"adbc.db2.current_schema": "EXPLICIT_SCHEMA"} + + ipv6_resolved = build_connection_config({"dsn": "DATABASE=testdb;HOSTNAME=::1;PORT=50000;UID=db2inst1;"}) + assert ipv6_resolved["uri"] == "db2://[::1]:50000/testdb" + + odbc_cfg = AdbcConfig( + connection_config={"dsn": "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=testdb;HOSTNAME=localhost;PORT=50000;"} + ) + assert _resolve_driver_name(odbc_cfg) == "adbc_driver_sqlite.dbapi.connect" + + +def test_adbc_driver_db2_routes_parameterized_non_query_via_executemany() -> None: + """Db2 ADBC routes parameterized non-queries through cursor.executemany across initial, cached, and script paths.""" + cursor = MagicMock() + cursor.description = None + cursor.rowcount = 1 + cursor.fetch_arrow_table.return_value = MagicMock( + column_names=["ID"], + to_pylist=lambda: [{"ID": 1}], + rename_columns=lambda cols: MagicMock(column_names=cols, to_pylist=lambda: [{cols[0]: 1}]), + ) + conn = MagicMock() + conn.adbc_get_info.return_value = {"vendor_name": "IBM Db2", "driver_name": "adbc_driver_db2"} + conn.cursor.return_value = cursor + + driver = AdbcDriver(conn, dialect="db2") + + driver.execute("CREATE TABLE items (id INT NOT NULL PRIMARY KEY, name VARCHAR(64))") + assert cursor.execute.call_count == 1 + assert cursor.executemany.call_count == 0 + + first_insert = driver.execute("INSERT INTO items (id, name) VALUES (?, ?)", (1, "alpha")) + assert first_insert.rows_affected == 1 + assert cursor.executemany.call_count == 1 + assert cursor.executemany.call_args_list[0].args == ("INSERT INTO items (id, name) VALUES (?, ?)", [[1, "alpha"]]) + + cached_insert = driver.execute("INSERT INTO items (id, name) VALUES (?, ?)", (2, "beta")) + assert cached_insert.rows_affected == 1 + assert cursor.executemany.call_count == 2 + assert cursor.executemany.call_args_list[1].args == ("INSERT INTO items (id, name) VALUES (?, ?)", [[2, "beta"]]) + + script_update = driver.execute_script("UPDATE items SET name = ? WHERE id = ?", ("gamma", 1)) + assert script_update.rows_affected == 1 + assert cursor.execute.call_count == 2 + assert cursor.execute.call_args_list[1].args == ("UPDATE items SET name = 'gamma' WHERE id = 1",) + assert cursor.executemany.call_count == 2 + + direct_single_script = driver.dispatch_execute_script( + cursor, SQL("UPDATE items SET name = ? WHERE id = ?", ("gamma", 1), statement_config=driver.statement_config) + ) + assert direct_single_script.rowcount_override == 1 + assert cursor.executemany.call_count == 3 + assert cursor.executemany.call_args_list[2].args == ("UPDATE items SET name = ? WHERE id = ?", [["gamma", 1]]) + + direct_multi_script = driver.dispatch_execute_script( + cursor, + SQL( + "CREATE TABLE extra (id INT); INSERT INTO items (id, name) VALUES (?, ?)", + (3, "delta"), + statement_config=driver.statement_config, + ), + ) + assert direct_multi_script.rowcount_override == 1 + assert cursor.execute.call_count == 3 + assert cursor.executemany.call_count == 4 + assert cursor.executemany.call_args_list[3].args == ("INSERT INTO items (id, name) VALUES (?, ?)", [[3, "delta"]]) + + named_delete = driver.execute("DELETE FROM items WHERE id = :item_id", {"item_id": 2}) + assert named_delete.rows_affected == 1 + assert cursor.executemany.call_count == 5 + assert cursor.executemany.call_args_list[4].args == ("DELETE FROM items WHERE id = ?", [[2]]) + + driver.execute("SELECT id FROM items WHERE id = ?", (1,)) + assert cursor.execute.call_count == 4 + assert cursor.executemany.call_count == 5 + + +def _resolve_driver_name(config: AdbcConfig) -> str: + """Resolve driver name from configuration.""" + return resolve_driver_name_from_config(config.connection_config) + + +def _get_connection_config_dict(config: AdbcConfig) -> dict[str, Any]: + """Build the normalized connection configuration.""" + return build_connection_config(config.connection_config) From f7856dd32dadbf057738816157adba0c37dba623 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 17:35:51 +0000 Subject: [PATCH 15/35] ci(integration): wire unlocked container images and snowflake shard target --- .github/workflows/ci.yml | 10 +++++----- Makefile | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4793effc..5f708cd2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -278,17 +278,17 @@ jobs: include: - shard: postgres-cockroach install_odbc: false - images: "postgres:18 pgvector/pgvector:pg18 paradedb/paradedb:latest-pg18 cockroachdb/cockroach:latest rustfs/rustfs:latest rustfs/rc:latest" + images: "postgres:18 pgvector/pgvector:pg18 paradedb/paradedb:latest-pg18 cockroachdb/cockroach:latest rustfs/rustfs:latest rustfs/rc:latest gcr.io/alloydb-omni/alloydbomni:18" pytest_targets: "tests/integration/adapters/postgres tests/integration/adapters/cockroach tests/integration/extensions tests/integration/observability" extra_pytest_flags: "" - shard: mysql-mariadb-embedded install_odbc: false - images: "mysql:8.4 rustfs/rustfs:latest rustfs/rc:latest" + images: "mysql:8.4 rustfs/rustfs:latest rustfs/rc:latest mariadb:11.8" pytest_targets: "tests/integration/adapters/mysql tests/integration/adapters/sqlite tests/integration/adapters/duckdb tests/integration/cli tests/integration/migrations" extra_pytest_flags: "" - shard: oracle install_odbc: false - images: "gvenzl/oracle-xe:18-slim-faststart gvenzl/oracle-free:23-slim-faststart" + images: "gvenzl/oracle-xe:18-slim-faststart gvenzl/oracle-free:23-slim-faststart container-registry.oracle.com/database/adb-free:latest-23ai" pytest_targets: "tests/integration/adapters/oracle" extra_pytest_flags: "" - shard: mssql-db2 @@ -298,8 +298,8 @@ jobs: extra_pytest_flags: "" - shard: cloud-adbc-storage install_odbc: false - images: "ghcr.io/goccy/bigquery-emulator:latest gcr.io/cloud-spanner-emulator/emulator:latest gizmodata/gizmosql:latest rustfs/rustfs:latest rustfs/rc:latest" - pytest_targets: "tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage" + images: "ghcr.io/goccy/bigquery-emulator:latest gcr.io/cloud-spanner-emulator/emulator:latest gizmodata/gizmosql:latest rustfs/rustfs:latest rustfs/rc:latest floci/floci-gcp:latest floci/floci:latest mcr.microsoft.com/azure-storage/azurite:latest embucket/embucket:latest" + pytest_targets: "tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage tests/integration/adapters/snowflake" extra_pytest_flags: "--run-bigquery-tests --run-spanner-tests" timeout-minutes: 30 steps: diff --git a/Makefile b/Makefile index 7c45b1e99..07bdc4045 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ SHARD_POSTGRES_COCKROACH := tests/integration/adapters/postgres tests/integratio SHARD_MYSQL_MARIADB_EMBEDDED := tests/integration/adapters/mysql tests/integration/adapters/sqlite tests/integration/adapters/duckdb tests/integration/cli tests/integration/migrations SHARD_ORACLE := tests/integration/adapters/oracle SHARD_MSSQL_DB2 := tests/integration/adapters/mssql tests/integration/adapters/db2 -SHARD_CLOUD_ADBC_STORAGE := tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage +SHARD_CLOUD_ADBC_STORAGE := tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage tests/integration/adapters/snowflake # ----------------------------------------------------------------------------- # Display Formatting and Colors @@ -216,7 +216,7 @@ test-integration-mssql-db2: ## Run SQL Server and Db2 in @echo "${OK} mssql-db2 integration shard complete ✨" .PHONY: test-integration-cloud-adbc-storage -test-integration-cloud-adbc-storage: ## Run BigQuery, Spanner, GizmoSQL, and storage shard +test-integration-cloud-adbc-storage: ## Run BigQuery, Spanner, GizmoSQL, Snowflake, and storage shard @echo "${INFO} Running cloud-adbc-storage integration shard... 🧪" @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup --run-bigquery-tests --run-spanner-tests $(SHARD_CLOUD_ADBC_STORAGE) @echo "${OK} cloud-adbc-storage integration shard complete ✨" From 01b61c65503948263f1abf573bcc40958c5cd766 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 19:42:05 +0000 Subject: [PATCH 16/35] fix(postgres,tests): preserve floci HEAD content-length and align auto pooler_mode --- sqlspec/adapters/asyncpg/config.py | 7 ++-- sqlspec/adapters/psqlpy/config.py | 5 ++- sqlspec/adapters/psycopg/core.py | 22 ++++++++--- tests/conftest.py | 4 +- .../oracle/oracledb/test_adb_wallet.py | 22 +---------- tests/integration/fixtures/cloud_storage.py | 25 ++++++------ tests/integration/fixtures/oracle.py | 38 ++++++++++--------- tests/integration/storage/test_integration.py | 9 ++++- .../test_adbc_config_normalization.py | 9 +++++ .../unit/adapters/test_asyncpg/test_config.py | 15 ++++++++ .../unit/adapters/test_psqlpy/test_config.py | 22 ++++++++++- .../unit/adapters/test_psycopg/test_config.py | 28 ++++++++++++++ 12 files changed, 143 insertions(+), 63 deletions(-) diff --git a/sqlspec/adapters/asyncpg/config.py b/sqlspec/adapters/asyncpg/config.py index 19db0a191..47c817478 100644 --- a/sqlspec/adapters/asyncpg/config.py +++ b/sqlspec/adapters/asyncpg/config.py @@ -351,11 +351,12 @@ def __init__( features_dict["pgbouncer_mode"] = True if features_dict.get("pooler_mode") in {None, "session", "auto"}: features_dict["pooler_mode"] = "transaction" - if features_dict.get("pooler_mode") in {"transaction", "statement"} or bool( + is_transaction_pooler = features_dict.get("pooler_mode") in {"transaction", "statement"} or bool( features_dict.get("pgbouncer") or features_dict.get("pgbouncer_mode") - ): + ) + if is_transaction_pooler: normalized_conn_cfg.setdefault("statement_cache_size", 0) - if bool(features_dict.get("enable_alloydb_columnar", False)) and features_dict.get("pooler_mode") == "session": + if bool(features_dict.get("enable_alloydb_columnar", False)) and not is_transaction_pooler: merged_settings = dict(self._explicit_server_settings) if self._explicit_server_settings is not None else {} merged_settings.setdefault("google_columnar_engine.enable_columnar_scan", "on") normalized_conn_cfg["server_settings"] = merged_settings diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py index b1a0b71fb..84682ba78 100644 --- a/sqlspec/adapters/psqlpy/config.py +++ b/sqlspec/adapters/psqlpy/config.py @@ -285,7 +285,10 @@ def __init__( statement_config, driver_features = apply_driver_features(statement_config, driver_features) features_dict = dict(driver_features) if driver_features else {} - if bool(features_dict.get("enable_alloydb_columnar", False)) and features_dict.get("pooler_mode") == "session": + is_transaction_pooler = features_dict.get("pooler_mode") in {"transaction", "statement"} or bool( + features_dict.get("pgbouncer") or features_dict.get("pgbouncer_mode") + ) + if bool(features_dict.get("enable_alloydb_columnar", False)) and not is_transaction_pooler: columnar_opt = "-c google_columnar_engine.enable_columnar_scan=on" existing_options = connection_config.get("options") if isinstance(existing_options, str) and existing_options.strip(): diff --git a/sqlspec/adapters/psycopg/core.py b/sqlspec/adapters/psycopg/core.py index 9653411cf..4459e54b0 100644 --- a/sqlspec/adapters/psycopg/core.py +++ b/sqlspec/adapters/psycopg/core.py @@ -28,6 +28,7 @@ ConnectionTimeoutError, DeadlockError, ForeignKeyViolationError, + ImproperConfigurationError, IntegrityError, NotNullViolationError, OperationalError, @@ -178,12 +179,23 @@ def apply_driver_features( features.setdefault("enable_paradedb", True) features.setdefault("enable_pg_textsearch", True) features.setdefault("enable_alloydb_columnar", False) + pgbouncer_enabled = bool(features.get("pgbouncer") or features.get("pgbouncer_mode")) + features["pgbouncer"] = pgbouncer_enabled + features["pgbouncer_mode"] = pgbouncer_enabled raw_pooler_mode = features.get("pooler_mode") - if raw_pooler_mode is None and (features.get("pgbouncer") or features.get("pgbouncer_mode")): - raw_pooler_mode = "transaction" - features["pooler_mode"] = raw_pooler_mode or "session" - features.setdefault("pgbouncer", False) - features.setdefault("pgbouncer_mode", False) + if raw_pooler_mode is None: + features["pooler_mode"] = "transaction" if pgbouncer_enabled else "session" + else: + normalized_mode = str(raw_pooler_mode).lower() + if normalized_mode not in {"session", "transaction", "statement", "auto"}: + msg = ( + f"Invalid pooler_mode: {raw_pooler_mode!r}. Expected 'session', 'transaction', 'statement', or 'auto'." + ) + raise ImproperConfigurationError(msg) + features["pooler_mode"] = "transaction" if normalized_mode == "auto" and pgbouncer_enabled else normalized_mode + if features["pooler_mode"] in {"transaction", "statement"}: + features["pgbouncer"] = True + features["pgbouncer_mode"] = True features.setdefault("direct_dsn", None) parameter_config = _parameter_config(driver_profile, serializer, deserializer) diff --git a/tests/conftest.py b/tests/conftest.py index 70aea1233..b413869da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -78,13 +78,15 @@ def should_run_optional_cloud_suite(cli_enabled: bool, env_var: str) -> bool: def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None: - """Skip optional adapter unit trees before importing unavailable dependencies.""" + """Skip optional adapter trees before importing unavailable dependencies.""" _ = config path = str(collection_path) if "tests/unit/adapters/test_mssql_python" in path and not _module_is_available("mssql_python"): return True if "tests/unit/adapters/test_spanner" in path and not _module_is_available("google.cloud.spanner_v1"): return True + if "tests/integration/adapters/oracle/oracledb" in path and not _module_is_available("oracledb"): + return True return None diff --git a/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py b/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py index 00698cc19..732967dda 100644 --- a/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py +++ b/tests/integration/adapters/oracle/oracledb/test_adb_wallet.py @@ -7,8 +7,8 @@ import pytest from sqlspec.adapters.oracledb import OracleAsyncConfig, OraclePoolParams, OracleSyncConfig -from sqlspec.adapters.oracledb.core import parse_tnsnames_ora from sqlspec.exceptions import SQLSpecError +from tests.integration.fixtures.oracle import resolve_oracle_adb_mtls_alias if TYPE_CHECKING: from pathlib import Path @@ -58,7 +58,7 @@ def test_oracle_adb_extracted_wallet_dir_service_name( oracle_adb_service: "OracleADBService", oracle_adb_wallet_dir: "Path" ) -> None: """Verify direct extracted wallet directory connection using TNS alias resolution.""" - tns_alias = _resolve_mtls_alias(oracle_adb_service, oracle_adb_wallet_dir) + tns_alias = resolve_oracle_adb_mtls_alias(oracle_adb_service, oracle_adb_wallet_dir) config = OracleSyncConfig( connection_config=OraclePoolParams( dsn=tns_alias, @@ -106,21 +106,3 @@ def test_oracle_adb_dbms_cloud_capability_probe(oracle_adb_sync_config: OracleSy assert "DBMS_CLOUD" in error_text or "ORA-" in error_text or "PLS-" in error_text finally: driver.execute(f"DROP TABLE {table_name} PURGE") - - -def _resolve_mtls_alias(oracle_adb_service: "OracleADBService", wallet_dir: "Path") -> str: - """Select an mTLS TCPS alias from the extracted ADB wallet tnsnames.ora.""" - entries = parse_tnsnames_ora(wallet_dir / "tnsnames.ora") - candidates = ( - oracle_adb_service.service_name.lower(), - oracle_adb_service.service_name.lower().split(".", 1)[0], - *(alias.lower() for alias in oracle_adb_service.service_names), - ) - for candidate in candidates: - descriptor = entries.get(candidate, "") - if "tcps" in descriptor.lower(): - return candidate - for alias, descriptor in entries.items(): - if "tcps" in descriptor.lower(): - return alias - return oracle_adb_service.service_names[0] diff --git a/tests/integration/fixtures/cloud_storage.py b/tests/integration/fixtures/cloud_storage.py index 67c21bf5c..381df974f 100644 --- a/tests/integration/fixtures/cloud_storage.py +++ b/tests/integration/fixtures/cloud_storage.py @@ -4,7 +4,7 @@ import email.utils import http.client import http.server -import os +import importlib import threading import time import urllib.error @@ -16,12 +16,10 @@ from pytest_databases.docker.floci import FlociGCPService from pytest_databases.helpers import get_xdist_worker_num -from sqlspec.utils.module_loader import import_optional, import_optional_attr +from sqlspec.utils.module_loader import import_optional_attr from sqlspec.utils.serializers import from_json from tests.fixtures.rustfs import ensure_s3_bucket -os.environ.setdefault("GCSFS_EXPERIMENTAL_ZB_HNS_SUPPORT", "false") - if TYPE_CHECKING: from collections.abc import Generator @@ -37,9 +35,6 @@ FSSpecBackend = import_optional_attr("sqlspec.storage.backends.fsspec", "FSSpecBackend") ObStoreBackend = import_optional_attr("sqlspec.storage.backends.obstore", "ObStoreBackend") -fsspec_module: Any = import_optional("fsspec") -CoreGCSFileSystem: Any = import_optional_attr("gcsfs.core", "GCSFileSystem") - FLOCI_GCP_GATEWAY_PORT = 4588 __all__ = ( @@ -118,13 +113,18 @@ def floci_gcs_obstore(floci_gcp_service: "FlociGCPService", floci_gcs_bucket_nam @pytest.fixture -def floci_gcs_fsspec(floci_gcp_service: "FlociGCPService", floci_gcs_bucket_name: str) -> "ObjectStoreProtocol": +def floci_gcs_fsspec( + floci_gcp_service: "FlociGCPService", floci_gcs_bucket_name: str, monkeypatch: pytest.MonkeyPatch +) -> "ObjectStoreProtocol": """Provide an FSSpecBackend connected to the Floci GCP Storage emulator.""" pytest.importorskip("fsspec") pytest.importorskip("gcsfs") - if fsspec_module is not None and CoreGCSFileSystem is not None: - fsspec_module.register_implementation("gs", CoreGCSFileSystem, clobber=True) - fsspec_module.register_implementation("gcs", CoreGCSFileSystem, clobber=True) + monkeypatch.setenv("GCSFS_EXPERIMENTAL_ZB_HNS_SUPPORT", "false") + core_gcs_fs: Any = import_optional_attr("gcsfs.core", "GCSFileSystem") + fsspec_registry_mod: Any = importlib.import_module("fsspec.registry") + if core_gcs_fs is not None: + monkeypatch.setitem(fsspec_registry_mod._registry, "gs", core_gcs_fs) + monkeypatch.setitem(fsspec_registry_mod._registry, "gcs", core_gcs_fs) return FSSpecBackend(f"gs://{floci_gcs_bucket_name}", token="anon", endpoint_url=floci_gcp_service.endpoint_url) @@ -285,7 +285,8 @@ def _translate_request(self) -> "tuple[str, str, dict[str, str]]": headers["Content-Length"] = "0" return "POST", copy_path, headers headers = {key: val for key, val in self.headers.items() if key.lower() not in excluded_headers} - return self.command, self.path, headers + upstream_method = "GET" if self.command == "HEAD" else self.command + return upstream_method, self.path, headers @staticmethod def _extract_etag(response_body: bytes) -> str: diff --git a/tests/integration/fixtures/oracle.py b/tests/integration/fixtures/oracle.py index f2cc27469..a9aadd568 100644 --- a/tests/integration/fixtures/oracle.py +++ b/tests/integration/fixtures/oracle.py @@ -47,6 +47,7 @@ "oracle_connection_config", "oracle_sync_config", "oracle_sync_session", + "resolve_oracle_adb_mtls_alias", ) @@ -234,6 +235,24 @@ async def oracle_adb_async_config( await config.close_pool() +def resolve_oracle_adb_mtls_alias(oracle_adb_service: OracleADBService, wallet_dir: "Path") -> str: + """Select an mTLS TCPS alias from the extracted ADB wallet tnsnames.ora.""" + entries = parse_tnsnames_ora(wallet_dir / "tnsnames.ora") + candidates = ( + oracle_adb_service.service_name.lower(), + oracle_adb_service.service_name.lower().split(".", 1)[0], + *(alias.lower() for alias in oracle_adb_service.service_names), + ) + for candidate in candidates: + descriptor = entries.get(candidate, "") + if "tcps" in descriptor.lower(): + return candidate + for alias, descriptor in entries.items(): + if "tcps" in descriptor.lower(): + return alias + return oracle_adb_service.service_names[0] + + def _oracle_pool_params(oracle_service: OracleService) -> "OraclePoolParams": return cast( "OraclePoolParams", @@ -254,7 +273,7 @@ def _oracle_pool_params(oracle_service: OracleService) -> "OraclePoolParams": def _oracle_adb_pool_params( oracle_adb_service: OracleADBService, wallet_dir: "Path", wallet_zip_path: "Path" ) -> "OraclePoolParams": - tns_alias = _resolve_oracle_adb_mtls_alias(oracle_adb_service, wallet_dir) + tns_alias = resolve_oracle_adb_mtls_alias(oracle_adb_service, wallet_dir) return cast( "OraclePoolParams", { @@ -271,23 +290,6 @@ def _oracle_adb_pool_params( ) -def _resolve_oracle_adb_mtls_alias(oracle_adb_service: OracleADBService, wallet_dir: "Path") -> str: - entries = parse_tnsnames_ora(wallet_dir / "tnsnames.ora") - candidates = ( - oracle_adb_service.service_name.lower(), - oracle_adb_service.service_name.lower().split(".", 1)[0], - *(alias.lower() for alias in oracle_adb_service.service_names), - ) - for candidate in candidates: - descriptor = entries.get(candidate, "") - if "tcps" in descriptor.lower(): - return candidate - for alias, descriptor in entries.items(): - if "tcps" in descriptor.lower(): - return alias - return oracle_adb_service.service_names[0] - - def _load_or_synthesize_adb_p12_bytes(wallet_dir: "Path", wallet_password: str) -> bytes: p12_path = wallet_dir / "ewallet.p12" if p12_path.is_file(): diff --git a/tests/integration/storage/test_integration.py b/tests/integration/storage/test_integration.py index 159ae54cc..28d3c45c1 100644 --- a/tests/integration/storage/test_integration.py +++ b/tests/integration/storage/test_integration.py @@ -711,10 +711,15 @@ async def _assert_cloud_backend_operations(backend: "ObjectStoreProtocol", prefi backend.write_bytes_sync(sync_path, TEST_BINARY_CONTENT) assert backend.exists_sync(sync_path) is True assert backend.read_bytes_sync(sync_path) == TEST_BINARY_CONTENT + sync_meta = backend.get_metadata_sync(sync_path) + assert sync_meta["size"] == len(TEST_BINARY_CONTENT) - await backend.write_bytes_async(async_path, TEST_TEXT_CONTENT.encode("utf-8")) + encoded_text = TEST_TEXT_CONTENT.encode("utf-8") + await backend.write_bytes_async(async_path, encoded_text) assert await backend.exists_async(async_path) is True - assert await backend.read_bytes_async(async_path) == TEST_TEXT_CONTENT.encode("utf-8") + assert await backend.read_bytes_async(async_path) == encoded_text + async_meta = await backend.get_metadata_async(async_path) + assert async_meta["size"] == len(encoded_text) backend.copy_sync(sync_path, copy_path) assert backend.exists_sync(copy_path) is True diff --git a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py index fb91359c7..656a6e23c 100644 --- a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py +++ b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py @@ -773,6 +773,15 @@ def test_adbc_driver_db2_routes_parameterized_non_query_via_executemany() -> Non assert cursor.execute.call_count == 4 assert cursor.executemany.call_count == 5 + class _ArrowStreamStub: + def __arrow_c_stream__(self, requested_schema: object = None) -> object: + return requested_schema + + arrow_stream = _ArrowStreamStub() + AdbcDriver._execute_db2_non_query(cursor, "INSERT INTO items (id, name) VALUES (?, ?)", arrow_stream) + assert cursor.executemany.call_count == 6 + assert cursor.executemany.call_args_list[5].args == ("INSERT INTO items (id, name) VALUES (?, ?)", arrow_stream) + def _resolve_driver_name(config: AdbcConfig) -> str: """Resolve driver name from configuration.""" diff --git a/tests/unit/adapters/test_asyncpg/test_config.py b/tests/unit/adapters/test_asyncpg/test_config.py index ce81b2236..b1d0cc43a 100644 --- a/tests/unit/adapters/test_asyncpg/test_config.py +++ b/tests/unit/adapters/test_asyncpg/test_config.py @@ -478,6 +478,21 @@ async def test_asyncpg_columnar_engine_session_vs_transaction() -> None: "google_columnar_engine.enable_columnar_scan": "on", } + auto_direct_config = AsyncpgConfig( + connection_config={"dsn": "postgresql://localhost/test"}, + driver_features={"pooler_mode": "auto", "enable_alloydb_columnar": True}, + ) + assert auto_direct_config.connection_config["server_settings"] == { + "google_columnar_engine.enable_columnar_scan": "on" + } + + auto_pgbouncer_config = AsyncpgConfig( + connection_config={"dsn": "postgresql://localhost/test"}, + driver_features={"pooler_mode": "auto", "pgbouncer": True, "enable_alloydb_columnar": True}, + ) + assert auto_pgbouncer_config.driver_features["pooler_mode"] == "transaction" + assert "server_settings" not in auto_pgbouncer_config.connection_config + tx_config = AsyncpgConfig( connection_config={"dsn": "postgresql://localhost/test"}, driver_features={"pooler_mode": "transaction", "enable_alloydb_columnar": True}, diff --git a/tests/unit/adapters/test_psqlpy/test_config.py b/tests/unit/adapters/test_psqlpy/test_config.py index 79703d08a..e3494c315 100644 --- a/tests/unit/adapters/test_psqlpy/test_config.py +++ b/tests/unit/adapters/test_psqlpy/test_config.py @@ -1,6 +1,6 @@ """Psqlpy configuration tests covering statement config builders.""" -from typing import cast +from typing import Any, cast from unittest.mock import AsyncMock import pytest @@ -15,6 +15,7 @@ ) from sqlspec.adapters.psqlpy.driver import PsqlpyDriver from sqlspec.core import SQL, StatementConfig +from sqlspec.exceptions import ImproperConfigurationError class _ExtensionQueryResult: @@ -328,6 +329,25 @@ async def test_psqlpy_derive_direct_config_and_columnar_engine() -> None: base_config = PsqlpyConfig(connection_config={"dsn": "postgresql://localhost:5432/test"}) assert base_config.derive_direct_config() is base_config + auto_direct_config = PsqlpyConfig( + connection_config={"dsn": "postgresql://localhost:5432/test"}, + driver_features={"pooler_mode": "auto", "enable_alloydb_columnar": True}, + ) + assert auto_direct_config.connection_config["options"] == "-c google_columnar_engine.enable_columnar_scan=on" + + auto_pgbouncer_config = PsqlpyConfig( + connection_config={"dsn": "postgresql://pooler:6432/test"}, + driver_features={"pooler_mode": "auto", "pgbouncer": True, "enable_alloydb_columnar": True}, + ) + assert auto_pgbouncer_config.driver_features["pooler_mode"] == "transaction" + assert "options" not in auto_pgbouncer_config.connection_config + + with pytest.raises(ImproperConfigurationError, match="Invalid pooler_mode"): + PsqlpyConfig( + connection_config={"dsn": "postgresql://localhost:5432/test"}, + driver_features=cast("Any", {"pooler_mode": "invalid"}), + ) + pooled_config = PsqlpyConfig( connection_config={"dsn": "postgresql://pooler:6432/test"}, driver_features={ diff --git a/tests/unit/adapters/test_psycopg/test_config.py b/tests/unit/adapters/test_psycopg/test_config.py index c1836b384..af34e66a6 100644 --- a/tests/unit/adapters/test_psycopg/test_config.py +++ b/tests/unit/adapters/test_psycopg/test_config.py @@ -748,6 +748,34 @@ async def test_psycopg_enable_alloydb_columnar_and_derive_direct_config() -> Non assert "-c google_columnar_engine.enable_columnar_scan=on" in session_sync.connection_config["options"] assert session_sync.derive_direct_config() is session_sync + auto_sync = PsycopgSyncConfig( + connection_config={"conninfo": "postgresql://localhost/db"}, + driver_features={"enable_alloydb_columnar": True, "pooler_mode": "auto"}, + ) + assert "-c google_columnar_engine.enable_columnar_scan=on" in auto_sync.connection_config["options"] + + auto_pgbouncer_sync = PsycopgSyncConfig( + connection_config={"conninfo": "postgresql://pooler:6432/db"}, + driver_features={"enable_alloydb_columnar": True, "pooler_mode": "auto", "pgbouncer": True}, + ) + assert auto_pgbouncer_sync.driver_features["pooler_mode"] == "transaction" + assert auto_pgbouncer_sync.driver_features["pgbouncer"] is True + assert auto_pgbouncer_sync.driver_features["pgbouncer_mode"] is True + assert "options" not in auto_pgbouncer_sync.connection_config + assert auto_pgbouncer_sync.connection_config["prepare_threshold"] is None + + with pytest.raises(ImproperConfigurationError, match="Invalid pooler_mode"): + PsycopgSyncConfig( + connection_config={"conninfo": "postgresql://localhost/db"}, + driver_features=cast("Any", {"pooler_mode": "invalid"}), + ) + + with pytest.raises(ImproperConfigurationError, match="Invalid pooler_mode"): + PsycopgAsyncConfig( + connection_config={"conninfo": "postgresql://localhost/db"}, + driver_features=cast("Any", {"pooler_mode": "invalid"}), + ) + txn_sync = PsycopgSyncConfig( connection_config={"conninfo": "postgresql://pooler:6432/db"}, driver_features={ From 3241836e7af9c0d6b103e6a438b2a9ba4aa94ad8 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 21:01:17 +0000 Subject: [PATCH 17/35] docs(adapters): update contributor and adapter testing guides --- tests/integration/adapters/README.md | 60 ++++++++++++++++++---------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/tests/integration/adapters/README.md b/tests/integration/adapters/README.md index 5c6ab78f0..a73ad1e34 100644 --- a/tests/integration/adapters/README.md +++ b/tests/integration/adapters/README.md @@ -1,9 +1,9 @@ # Database-Family Adapter Tests Integration tests are organized by the database being exercised: `sqlite/`, `mysql/`, `postgres/`, -`cockroach/`, `oracle/`, `mssql/`, `duckdb/`, `bigquery/`, `spanner/`, and `gizmosql/`. Each family -collects its portable cases from `test_shared.py` and keeps irreducible driver behavior below -`//`. +`cockroach/`, `oracle/`, `mssql/`, `db2/`, `duckdb/`, `bigquery/`, `spanner/`, `gizmosql/`, and +`snowflake/`. Each family collects its portable cases from `test_shared.py` and keeps irreducible +driver behavior below `//`. Reusable test templates, behaviors, cases, assertions, and schema inputs live in the non-collected `_shared/` package. A behavior is implemented once and parametrized over every capable driver, while @@ -208,16 +208,19 @@ Current residual inventory: | Area | Local files | Why they stay local | | --- | --- | --- | -| Oracle type handlers | `oracle/oracledb/test_msgspec_clob.py`, `test_numpy_vectors.py`, `test_smart_lob_coercion.py`, `test_sparse_vectors.py`, `test_uuid_binary.py` | Oracle-specific LOB/CLOB/BLOB hydration, NumPy/vector type handlers, sparse vector passthrough, and RAW/VARCHAR UUID coexistence. Portable vector, UUID, result, JSON, PL/SQL, and StatementStack behavior is contract-owned. | -| Oracle direct/load/driver specifics | `oracle/oracledb/test_direct_path_load.py`, `test_driver.py`, `test_execute_many.py`, `test_features.py`, `test_migrations.py` | Direct-path loading, shared sync/async Oracle locking behavior, native pool protocols, and Oracle-specific migration/feature surfaces that do not generalize without adapter-name conditionals. | -| ADBC backend residuals | `sqlite/adbc/`, `duckdb/adbc/`, `postgres/adbc/`, `bigquery/adbc/` | Raw ADBC connection metadata, backend-native SQL, dialect fallback, and backend-specific recovery. The non-collected implementation lives in `_shared/adbc_*.py`; each database family imports only its own tests. | -| GizmoSQL ADBC | `gizmosql/adbc/` | GizmoSQL FlightSQL service behavior, result streams, Arrow storage, migration, and dictionary behavior. | -| BigQuery cloud/analytics specifics | `bigquery/bigquery/test_arrow.py`, `test_config.py`, `test_driver.py`, `test_parameter_variants.py`, `test_vector_functions.py` | Emulator/native BigQuery project-dataset setup, job controls, parameter forms, and vector/analytics behavior beyond the active BigQuery contract case. | -| Spanner GoogleSQL | `spanner/spanner/test_arrow.py`, `test_batch_write_api.py`, `test_bytes_direct.py`, `test_crud_operations.py`, `test_driver.py`, `test_exceptions.py`, `test_execute_many.py`, `test_explain.py`, `test_load_from_arrow_mutations.py`, `test_parameter_variants.py`, `test_session_defaults.py` | Spanner needs admin-API DDL, separate read/write sessions, SDK BYTES encoding, mutation transports, and `query_mode=PLAN`; it remains deferred until the contract table fixture can preserve those semantics safely. | -| Spangres placeholders | `spanner/spanner/test_spangres_driver.py`, `test_spangres_parameter_styles.py` | Runtime Spangres coverage needs PostgreSQL-dialect Spanner fixtures that do not exist yet. The files keep only executable dialect/default-style documentation assertions. | -| Google Cloud asyncpg connectors | `postgres/asyncpg/test_cloud_connectors.py` | Cloud SQL and AlloyDB connector authentication, IAM, private-IP, and instance URI setup require real Google Cloud instances; shared contracts cover PostgreSQL behavior after a pool exists. | -| PostgreSQL-family driver quirks | `postgres/asyncpg/test_driver.py`, `postgres/psqlpy/test_driver.py`, `postgres/psycopg/test_driver.py`, `postgres/psycopg/test_async_copy.py`, `*/extensions/events/test_listen_notify.py` | Driver-specific cursor/prepared statement/COPY behavior plus native LISTEN/NOTIFY concurrency and durable-hybrid regressions. Portable native delivery, metadata, backend selection, and ack behavior is contract-owned. | -| Adapter extension storage details | `*/extensions/adk/test_owner_id_column.py`, `*/extensions/adk/test_memory_store.py`, `*/extensions/litestar/test_numpy_serialization.py`, `spanner/spanner/extensions/adk/test_adk_store.py`, `spanner/spanner/extensions/litestar/test_store.py`, `spanner/spanner/extensions/events/test_queue_backend.py` | Extension storage/serialization details and Spanner extension stores that need adapter-specific DDL/session fixtures; generic ADK/events/Litestar behavior is handled by the extension contracts for active cases. | +| Oracle type handlers | `oracle/oracledb/test_json_storage_modes.py`, `oracle/oracledb/test_lob_fetch_parity.py`, `oracle/oracledb/test_msgspec_clob.py`, `oracle/oracledb/test_numpy_vectors.py`, `oracle/oracledb/test_smart_lob_coercion.py`, `oracle/oracledb/test_sparse_vectors.py`, `oracle/oracledb/test_uuid_binary.py` | Oracle-specific LOB/CLOB/BLOB hydration, 18c vs 23ai JSON storage rungs, NumPy/vector type handlers, sparse vector passthrough, and RAW/VARCHAR UUID coexistence. Portable vector, UUID, result, JSON, PL/SQL, and StatementStack behavior is contract-owned. | +| Oracle direct/load/driver & ADB wallet | `oracle/oracledb/test_adb_wallet.py`, `oracle/oracledb/test_direct_path_load.py`, `oracle/oracledb/test_driver.py`, `oracle/oracledb/test_execute_many.py`, `oracle/oracledb/test_features.py`, `oracle/oracledb/test_migrations.py`, `oracle/oracledb/test_stack_transaction.py` | Autonomous Database Free 23ai mTLS `wallet_zip` (`ewallet.p12` to `ewallet.pem`) and `DBMS_CLOUD` checks, direct-path loading, shared sync/async Oracle locking behavior, native pool protocols, and Oracle-specific migration/feature surfaces. | +| ADBC backend residuals | `sqlite/adbc/`, `duckdb/adbc/`, `postgres/adbc/`, `bigquery/adbc/`, `oracle/adbc/test_adbc_oracle.py`, `db2/adbc/test_adbc_db2.py`, `snowflake/adbc/test_adbc_snowflake.py` | Raw ADBC connection metadata, pure-Go enterprise ADBC drivers (`adbc-driver-oracle`, `adbc-driver-db2`, `adbc-driver-snowflake`), backend-native SQL, dialect fallback, and backend-specific recovery. Shared helpers live in `_shared/adbc_*.py`; each database family imports only its own tests. | +| GizmoSQL ADBC | `gizmosql/adbc/` | GizmoSQL FlightSQL service behavior (`gizmosql_service` and `gizmosql_sqlite_service`), result streams, Arrow storage, migration, and dictionary behavior. | +| IBM Db2 adapter residuals | `db2/test_adapter.py` | Db2 Community Edition catalog, CLI connection-keyword normalization, and dialect-specific lock/register behavior. | +| MySQL & MariaDB residuals | `mysql/test_adk_residuals.py`, `mysql/test_column_metadata.py`, `mysql/test_features.py`, `mysql/test_mariadb.py`, `mysql/test_table_fixtures.py` | Consolidated MySQL ADK schema/precision/foreign-key residuals across all MySQL drivers, MariaDB 11.8 (`mariadb_118_service`) compatibility, and MySQL table-fixture/column-metadata details. | +| CockroachDB residuals | `cockroach/test_column_metadata.py`, `cockroach/test_native_storage.py`, `cockroach/test_native_storage_capabilities.py`, `cockroach/test_retry.py`, `cockroach/test_table_fixtures.py` | CockroachDB `40001` serialization-failure retry isolation, native external storage bridges, and `unique_rowid()` sequence/metadata behavior. | +| BigQuery cloud/analytics specifics | `bigquery/bigquery/test_arrow.py`, `bigquery/bigquery/test_config.py`, `bigquery/bigquery/test_driver.py`, `bigquery/bigquery/test_native_export.py`, `bigquery/bigquery/test_parameter_variants.py`, `bigquery/bigquery/test_vector_functions.py` | Emulator/native BigQuery project-dataset setup, native storage export, job controls, parameter forms, and vector/analytics behavior beyond the active BigQuery contract case. | +| Spanner GoogleSQL | `spanner/spanner/test_arrow.py`, `spanner/spanner/test_async_driver.py`, `spanner/spanner/test_batch_write_api.py`, `spanner/spanner/test_crud_operations.py`, `spanner/spanner/test_driver.py`, `spanner/spanner/test_exceptions.py`, `spanner/spanner/test_execute_many.py`, `spanner/spanner/test_explain.py`, `spanner/spanner/test_load_from_arrow_mutations.py`, `spanner/spanner/test_parameter_variants.py`, `spanner/spanner/test_returning.py`, `spanner/spanner/test_session_defaults.py`, `spanner/spanner/test_transactions.py` | Spanner needs admin-API DDL, separate read/write sessions, SDK BYTES encoding, mutation transports, and `query_mode=PLAN`; it remains deferred until the contract table fixture can preserve those semantics safely. | +| Spangres placeholders | `spanner/spanner/test_spangres_driver.py`, `spanner/spanner/test_spangres_parameter_styles.py` | Runtime Spangres coverage needs PostgreSQL-dialect Spanner fixtures that do not exist yet. The files keep only executable dialect/default-style documentation assertions. | +| Google Cloud asyncpg & AlloyDB Omni | `postgres/asyncpg/test_cloud_connectors.py`, `postgres/test_alloydb_omni.py` | Cloud SQL and AlloyDB connector authentication plus AlloyDB Omni 18 (`alloydb_omni_18_service`) `enable_alloydb_columnar` session GUC verification across `asyncpg`, `psycopg`, and `psqlpy`. | +| PostgreSQL-family driver quirks | `postgres/asyncpg/test_driver.py`, `postgres/psqlpy/test_driver.py`, `postgres/psycopg/test_driver.py`, `postgres/psycopg/test_async_copy.py`, `postgres/*/extensions/events/test_listen_notify.py` | Driver-specific cursor/prepared statement/COPY behavior plus native LISTEN/NOTIFY concurrency and durable-hybrid regressions. Portable native delivery, metadata, backend selection, and ack behavior is contract-owned. | +| Adapter extension storage details | `*/*/extensions/adk/test_owner_id_column.py`, `*/*/extensions/adk/test_memory_store.py`, `sqlite/aiosqlite/extensions/litestar/test_numpy_serialization.py`, `spanner/spanner/extensions/adk/test_adk_artifact_store.py`, `spanner/spanner/extensions/adk/test_adk_store.py`, `spanner/spanner/extensions/litestar/test_store.py`, `spanner/spanner/extensions/events/test_queue_backend.py` | Extension storage/serialization details and Spanner extension stores (including GCS-backed ADK artifact version tracking against `floci_gcp_service`) that need adapter-specific DDL/session fixtures; generic ADK/events/Litestar behavior is handled by the extension contracts for active cases. | ## Adding A Case @@ -237,13 +240,30 @@ Current residual inventory: # One database family uv run pytest tests/integration/adapters/sqlite -# All shared family cases (service containers start through pytest-databases) -uv run pytest tests/integration/adapters/*/test_shared.py +# All shared family cases (service containers start through clientless pytest-databases) +uv run pytest -n 2 --dist=loadgroup tests/integration/adapters/*/test_shared.py + +# Service-group integration shards (matching CI test-integration matrix) +make test-integration-postgres-cockroach +make test-integration-mysql-mariadb-embedded +make test-integration-oracle +make test-integration-mssql-db2 +make test-integration-cloud-adbc-storage ``` -Service-backed adapters keep their opt-in marks and xdist groups when they move from deferred to -active rows. BigQuery integration runs in CI by default; local runs require -`SQLSPEC_ENABLE_BIGQUERY_TESTS=1` and `--run-bigquery-tests`. +`tests/integration/conftest.py` registers clientless `pytest_databases.docker.*` service plugins and +imports only `tests/integration/fixtures/.py` plugins whose required Python driver packages +resolve through `_FIXTURE_DRIVER_REQUIREMENTS`. + +Under `pytest-xdist` (`--dist=loadgroup`), `tests/integration/adapters/conftest.py` normalizes each +collected item to a single `xdist_group`: items carrying a dedicated secondary-container marker in +`_PRESERVED_CONTAINER_XDIST_GROUPS` (`alloydb`, `gizmosql_sqlite`, `mariadb`, `oracle_18c`, +`oracle_adb`, `paradedb`, `pgvector`, `snowflake`) keep that container group, while all other items +group under their database family. + +BigQuery and Spanner emulator suites run automatically in CI (`CI=true`). Local runs opt in with +either `--run-bigquery-tests` or `SQLSPEC_ENABLE_BIGQUERY_TESTS=1` for BigQuery, and either +`--run-spanner-tests` or `SQLSPEC_ENABLE_SPANNER_TESTS=1` for Spanner. -`suite_case_metadata_contract.py` includes a no-orphan guard for files removed by consolidation, so -shared behavior cannot silently reappear in driver-local files. +`suite_case_metadata_contract.py` includes a no-orphan guard (`CONTRACT_OWNED_REMOVED_TEST_PATHS`) +for files removed by consolidation, so shared behavior cannot silently reappear in driver-local files. From 39066766da4ace9e39ecf1b3827b7b331c37089b Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 26 Oct 2026 22:01:03 +0000 Subject: [PATCH 18/35] fix(adapters): handle gizmosql adbc defaults and python 3.13 oracle self-signed wallets --- sqlspec/adapters/adbc/core.py | 21 +++- sqlspec/adapters/oracledb/config.py | 11 ++ sqlspec/adapters/oracledb/core.py | 66 ++++++++++ .../test_adbc_config_normalization.py | 6 + tests/unit/adapters/test_adbc/test_core.py | 31 +++++ .../adapters/test_oracledb/test_config.py | 119 +++++++++++++++++- 6 files changed, 250 insertions(+), 4 deletions(-) diff --git a/sqlspec/adapters/adbc/core.py b/sqlspec/adapters/adbc/core.py index 273c6b615..ee56694f5 100644 --- a/sqlspec/adapters/adbc/core.py +++ b/sqlspec/adapters/adbc/core.py @@ -356,7 +356,7 @@ def detect_dialect(connection: Any, logger: Any | None = None, *, fallback_diale Detected dialect name, defaulting to ``postgres``. """ try: - driver_info = connection.adbc_get_info() + driver_info = _get_adbc_driver_info(connection) vendor_name = driver_info.get("vendor_name", "").lower() vendor_version = driver_info.get("vendor_version", "").lower() driver_name = driver_info.get("driver_name", "").lower() @@ -1960,12 +1960,31 @@ def _lift_flightsql_common_options( db_kwargs_dict.setdefault(f"{_FLIGHTSQL_RPC_CALL_HEADER_PREFIX}{hdr_key}", str(hdr_val)) +def _get_adbc_driver_info(connection: Any) -> "dict[Any, Any]": + """Return per-connection ADBC driver metadata without class-level cache bleed.""" + conn_type: Any = type(connection) + if "_get_info_cache" in conn_type.__dict__: + conn_dict = getattr(connection, "__dict__", None) + if isinstance(conn_dict, dict): + cached = conn_dict.get("_get_info_cache") + if isinstance(cached, dict): + return cached + for base_cls in conn_type.__mro__[1:]: + base_method = base_cls.__dict__.get("adbc_get_info") + if callable(base_method): + info = cast("dict[Any, Any]", base_method(connection)) + conn_dict["_get_info_cache"] = info + return info + return cast("dict[Any, Any]", connection.adbc_get_info()) + + def _lift_native_gizmosql_db_kwargs(config: "dict[str, Any]") -> None: db_kwargs = config.pop("db_kwargs", None) db_kwargs_dict: dict[str, Any] = dict(db_kwargs) if isinstance(db_kwargs, dict) else {} _tls_skip_verify_key, authorization_header_key = _resolve_flightsql_db_kwargs_keys() _lift_flightsql_common_options(config, db_kwargs_dict, authorization_header_key) config.pop("gizmosql_backend", None) + config.setdefault("autocommit", False) if db_kwargs_dict: config["db_kwargs"] = db_kwargs_dict diff --git a/sqlspec/adapters/oracledb/config.py b/sqlspec/adapters/oracledb/config.py index 0518391df..a088989ab 100644 --- a/sqlspec/adapters/oracledb/config.py +++ b/sqlspec/adapters/oracledb/config.py @@ -31,6 +31,7 @@ OracleTopologyConfig, apply_driver_features, build_connection_config, + build_wallet_ssl_context_if_needed, default_statement_config, execute_session_checkout_async, execute_session_checkout_sync, @@ -586,6 +587,11 @@ def _create_pool(self) -> "OracleSyncConnectionPool": client_config["config_dir"] = config["config_dir"] oracledb.init_oracle_client(**client_config) + if oracledb.is_thin_mode() and "ssl_context" not in config: + wallet_ssl_ctx = build_wallet_ssl_context_if_needed(config.get("wallet_location")) + if wallet_ssl_ctx is not None: + config["ssl_context"] = wallet_ssl_ctx + config.pop("threaded", None) config["session_callback"] = self._init_connection @@ -904,6 +910,11 @@ async def _create_pool(self) -> "OracleAsyncConnectionPool": msg = "OracleAsyncConfig only supports Thin mode; use OracleSyncConfig for Thick mode." raise ImproperConfigurationError(msg) + if oracledb.is_thin_mode() and "ssl_context" not in config: + wallet_ssl_ctx = build_wallet_ssl_context_if_needed(config.get("wallet_location")) + if wallet_ssl_ctx is not None: + config["ssl_context"] = wallet_ssl_ctx + config.pop("threaded", None) config["session_callback"] = self._init_connection diff --git a/sqlspec/adapters/oracledb/core.py b/sqlspec/adapters/oracledb/core.py index ce9a4f8f8..a71a32f30 100644 --- a/sqlspec/adapters/oracledb/core.py +++ b/sqlspec/adapters/oracledb/core.py @@ -7,6 +7,7 @@ import os import re import shutil +import ssl import stat import tempfile import threading @@ -112,6 +113,7 @@ "build_statement_config", "build_tns_description", "build_truncate_statement", + "build_wallet_ssl_context_if_needed", "coerce_large_parameters_async", "coerce_large_parameters_sync", "coerce_many_parameters_async", @@ -2424,6 +2426,43 @@ def build_dbms_cloud_copy_data_sql( return plsql, binds +def build_wallet_ssl_context_if_needed(wallet_location: Any) -> "ssl.SSLContext | None": + """Build a Thin-mode ``ssl.SSLContext`` for Oracle wallets with non-strict self-signed CAs. + + Oracle ``orapki -self_signed`` wallets (including Autonomous Database Free container + wallets) emit X.509 v1 or extension-less self-signed trust-anchor certificates that + OpenSSL rejects when Python 3.13+ enables ``ssl.VERIFY_X509_STRICT`` by default. + RFC 5280 X.509 v3 CA wallets with ``BasicConstraints(ca=True)`` return ``None`` so + the runtime's default strict verification remains intact. + + Args: + wallet_location: Configured Oracle wallet directory path, if any. + + Returns: + An ``ssl.SSLContext`` with ``VERIFY_X509_STRICT`` cleared while preserving + ``ssl.CERT_REQUIRED``, or ``None`` when the default ``ssl.SSLContext`` is compatible. + """ + if not wallet_location: + return None + location_str = str(wallet_location).strip() + if not location_str or location_str.upper() == "SYSTEM": + return None + pem_path = Path(location_str) / "ewallet.pem" + try: + if pem_path.is_symlink() or not pem_path.is_file(): + return None + except OSError: + return None + if not _wallet_pem_has_non_strict_ca(pem_path): + return None + strict_flag = ssl.VERIFY_X509_STRICT + ctx = ssl.create_default_context() + if not (ctx.verify_flags & strict_flag): + return None + ctx.verify_flags &= ~strict_flag + return ctx + + def _build_session_checkout_plan( config: "OracleSessionContextConfig | Mapping[str, Any]", ) -> "tuple[str | None, dict[str, Any], dict[str, str | None]]": @@ -2577,6 +2616,33 @@ def _build_session_release_plan( return f"BEGIN {' '.join(plsql_statements)} END;", bind_params, piggyback_attrs +def _wallet_pem_has_non_strict_ca(pem_path: Path) -> bool: + """Return True when ``ewallet.pem`` contains an X.509 v1 or non-CA self-signed certificate.""" + x509_mod: Any = import_optional("cryptography.x509") + if x509_mod is None: + return False + try: + pem_bytes = pem_path.read_bytes() + certificates = x509_mod.load_pem_x509_certificates(pem_bytes) + except (OSError, ValueError, TypeError): + return False + v1_version: Any = getattr(x509_mod.Version, "v1", None) + basic_constraints_cls: Any = x509_mod.BasicConstraints + for cert in certificates: + if v1_version is not None and cert.version == v1_version: + return True + if cert.subject == cert.issuer: + is_ca = False + try: + ext = cert.extensions.get_extension_for_class(basic_constraints_cls) + is_ca = bool(ext.value.ca) + except Exception: + is_ca = False + if not is_ca: + return True + return False + + def _cleanup_atexit_wallet_dirs() -> None: with _WALLET_DIR_LOCK: pending = list(_ATEXIT_WALLET_DIRS) diff --git a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py index 656a6e23c..ff02b1aee 100644 --- a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py +++ b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py @@ -627,6 +627,12 @@ def test_gizmosql_prefers_native_driver_when_installed(monkeypatch: Any) -> None resolved = _get_connection_config_dict(config) assert resolved["auth_type"] == "external" assert resolved["tls_skip_verify"] is True + assert resolved["autocommit"] is False + + explicit_autocommit = AdbcConfig( + connection_config={"driver_name": "gizmosql", "uri": "grpc+tls://localhost:31337", "autocommit": True} + ) + assert _get_connection_config_dict(explicit_autocommit)["autocommit"] is True def test_gizmosql_flightsql_fallback_lifts_auth_type_into_db_kwargs(monkeypatch: Any) -> None: diff --git a/tests/unit/adapters/test_adbc/test_core.py b/tests/unit/adapters/test_adbc/test_core.py index 9ec9835af..80f035874 100644 --- a/tests/unit/adapters/test_adbc/test_core.py +++ b/tests/unit/adapters/test_adbc/test_core.py @@ -325,6 +325,37 @@ def test_detect_dialect_uses_gizmosql_sqlite_vendor_version() -> None: assert result == "sqlite" +def test_detect_dialect_isolates_class_level_gizmosql_info_cache() -> None: + """Per-connection metadata must not inherit a class-level _get_info_cache from an earlier backend.""" + + class _BaseStubConnection: + def __init__(self, backend_label: str) -> None: + self._backend_label = backend_label + + def adbc_get_info(self) -> dict[str, str]: + return { + "vendor_name": "GizmoSQL", + "vendor_version": f"GizmoSQL {self._backend_label} backend", + "driver_name": "ADBC Flight SQL Driver", + } + + class _ClassCachingGizmoConnection(_BaseStubConnection): + _get_info_cache: dict[str, str] | None = None + + def adbc_get_info(self) -> dict[str, str]: + if self._get_info_cache is None: + _ClassCachingGizmoConnection._get_info_cache = super().adbc_get_info() + return self._get_info_cache or {} + + duckdb_conn = _ClassCachingGizmoConnection("DuckDB") + sqlite_conn = _ClassCachingGizmoConnection("SQLite") + + assert duckdb_conn.adbc_get_info()["vendor_version"] == "GizmoSQL DuckDB backend" + assert adbc_core.detect_dialect(duckdb_conn) == "duckdb" + assert adbc_core.detect_dialect(sqlite_conn) == "sqlite" + assert sqlite_conn.adbc_get_info()["vendor_version"] == "GizmoSQL SQLite backend" + + def test_handle_postgres_rollback_issues_rollback_for_pgvector() -> None: """pgvector is PostgreSQL-compatible and must receive rollback handling.""" executed: list[str] = [] diff --git a/tests/unit/adapters/test_oracledb/test_config.py b/tests/unit/adapters/test_oracledb/test_config.py index 1d411d0b7..3e7084f12 100644 --- a/tests/unit/adapters/test_oracledb/test_config.py +++ b/tests/unit/adapters/test_oracledb/test_config.py @@ -2,6 +2,7 @@ import io import os +import ssl import zipfile from collections.abc import Awaitable, Callable from datetime import UTC, datetime, timedelta @@ -745,12 +746,122 @@ async def test_oracle_shared_wallet_zip_refcounted_across_pools_and_derive_pool( assert not shared_dir.exists() -def _make_p12_wallet_bytes(password: bytes = b"WalletSecret123") -> bytes: +async def test_oracle_wallet_ssl_context_relaxes_x509_strict_only_for_non_ca_self_signed_wallets( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Thin pools clear VERIFY_X509_STRICT for orapki self-signed wallets while keeping RFC 5280 CA wallets strict.""" + orig_create_default_context = ssl.create_default_context + + def _strict_default_context(*args: Any, **kwargs: Any) -> ssl.SSLContext: + ctx = orig_create_default_context(*args, **kwargs) + ctx.verify_flags |= ssl.VERIFY_X509_STRICT + return ctx + + monkeypatch.setattr(ssl, "create_default_context", _strict_default_context) + + tns_text = ( + "mydb_tp = (DESCRIPTION=(ADDRESS=(PROTOCOL=TCPS)(HOST=adb.oraclecloud.com)(PORT=1522))" + "(CONNECT_DATA=(SERVICE_NAME=mydb_tp.adb.oraclecloud.com)))\n" + ) + self_signed_archive = _build_wallet_zip_bytes(_make_p12_wallet_bytes(b"WalletSecret123", is_ca=False), tns_text) + ca_v3_archive = _build_wallet_zip_bytes(_make_p12_wallet_bytes(b"WalletSecret123", is_ca=True), tns_text) + + created_sync_kwargs: list[dict[str, Any]] = [] + created_async_kwargs: list[dict[str, Any]] = [] + monkeypatch.setattr(oracle_config_module.oracledb, "is_thin_mode", lambda: True) + monkeypatch.setattr( + oracle_config_module.oracledb, + "create_pool", + lambda **kwargs: (created_sync_kwargs.append(kwargs), MagicMock())[1], + ) + monkeypatch.setattr( + oracle_config_module.oracledb, + "create_pool_async", + lambda **kwargs: (created_async_kwargs.append(kwargs), MagicMock(close=AsyncMock()))[1], + ) + + sync_self_signed = OracleSyncConfig( + connection_config={ + "user": "admin", + "password": "pw", + "wallet_zip": self_signed_archive, + "wallet_password": "WalletSecret123", + "tns_alias": "mydb_tp", + } + ) + async_self_signed = OracleAsyncConfig( + connection_config={ + "user": "admin", + "password": "pw", + "wallet_zip": self_signed_archive, + "wallet_password": "WalletSecret123", + "tns_alias": "mydb_tp", + } + ) + sync_ca_v3 = OracleSyncConfig( + connection_config={ + "user": "admin", + "password": "pw", + "wallet_zip": ca_v3_archive, + "wallet_password": "WalletSecret123", + "tns_alias": "mydb_tp", + } + ) + custom_ctx = orig_create_default_context() + sync_explicit_ctx = OracleSyncConfig( + connection_config={ + "user": "admin", + "password": "pw", + "wallet_zip": self_signed_archive, + "wallet_password": "WalletSecret123", + "tns_alias": "mydb_tp", + "ssl_context": custom_ctx, + } + ) + try: + sync_self_signed._create_pool() + await async_self_signed._create_pool() + sync_ca_v3._create_pool() + sync_explicit_ctx._create_pool() + + monkeypatch.setattr(oracle_config_module.oracledb, "is_thin_mode", lambda: False) + sync_self_signed._create_pool() + finally: + sync_self_signed._close_pool() + await async_self_signed._close_pool() + sync_ca_v3._close_pool() + sync_explicit_ctx._close_pool() + + sync_injected_ctx = created_sync_kwargs[0]["ssl_context"] + assert isinstance(sync_injected_ctx, ssl.SSLContext) + assert not (sync_injected_ctx.verify_flags & ssl.VERIFY_X509_STRICT) + assert sync_injected_ctx.verify_mode == ssl.CERT_REQUIRED + + async_injected_ctx = created_async_kwargs[0]["ssl_context"] + assert isinstance(async_injected_ctx, ssl.SSLContext) + assert not (async_injected_ctx.verify_flags & ssl.VERIFY_X509_STRICT) + assert async_injected_ctx.verify_mode == ssl.CERT_REQUIRED + + assert "ssl_context" not in created_sync_kwargs[1] + assert created_sync_kwargs[2]["ssl_context"] is custom_ctx + assert "ssl_context" not in created_sync_kwargs[3] + + +def _build_wallet_zip_bytes(p12_bytes: bytes, tns_text: str) -> bytes: + """Package PKCS#12 bytes and tnsnames.ora content into an in-memory wallet archive.""" + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + zf.writestr("ewallet.p12", p12_bytes) + zf.writestr("tnsnames.ora", tns_text) + return buf.getvalue() + + +def _make_p12_wallet_bytes(password: bytes = b"WalletSecret123", *, is_ca: bool = False) -> bytes: """Create a test PKCS#12 payload for Oracle config wallet tests.""" private_key = ec.generate_private_key(ec.SECP256R1()) subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "adb.oraclecloud.com")]) now = datetime.now(UTC) - cert = ( + builder = ( x509 .CertificateBuilder() .subject_name(subject) @@ -759,8 +870,10 @@ def _make_p12_wallet_bytes(password: bytes = b"WalletSecret123") -> bytes: .serial_number(x509.random_serial_number()) .not_valid_before(now - timedelta(minutes=5)) .not_valid_after(now + timedelta(days=1)) - .sign(private_key, hashes.SHA256()) ) + if is_ca: + builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + cert = builder.sign(private_key, hashes.SHA256()) return pkcs12.serialize_key_and_certificates( name=b"adb", key=private_key, From 052a7ca9b881455347f3e5d46c65e73a9bfa2380 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Tue, 27 Oct 2026 16:59:32 +0000 Subject: [PATCH 19/35] test(oracledb): satisfy mypy func-returns-value in wallet ssl_context test --- .../adapters/test_oracledb/test_config.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/unit/adapters/test_oracledb/test_config.py b/tests/unit/adapters/test_oracledb/test_config.py index 3e7084f12..18d25eb13 100644 --- a/tests/unit/adapters/test_oracledb/test_config.py +++ b/tests/unit/adapters/test_oracledb/test_config.py @@ -768,17 +768,18 @@ def _strict_default_context(*args: Any, **kwargs: Any) -> ssl.SSLContext: created_sync_kwargs: list[dict[str, Any]] = [] created_async_kwargs: list[dict[str, Any]] = [] + + def _fake_create_pool(**kwargs: Any) -> MagicMock: + created_sync_kwargs.append(kwargs) + return MagicMock() + + def _fake_create_pool_async(**kwargs: Any) -> MagicMock: + created_async_kwargs.append(kwargs) + return MagicMock(close=AsyncMock()) + monkeypatch.setattr(oracle_config_module.oracledb, "is_thin_mode", lambda: True) - monkeypatch.setattr( - oracle_config_module.oracledb, - "create_pool", - lambda **kwargs: (created_sync_kwargs.append(kwargs), MagicMock())[1], - ) - monkeypatch.setattr( - oracle_config_module.oracledb, - "create_pool_async", - lambda **kwargs: (created_async_kwargs.append(kwargs), MagicMock(close=AsyncMock()))[1], - ) + monkeypatch.setattr(oracle_config_module.oracledb, "create_pool", _fake_create_pool) + monkeypatch.setattr(oracle_config_module.oracledb, "create_pool_async", _fake_create_pool_async) sync_self_signed = OracleSyncConfig( connection_config={ From b0af1b1244177b7277a9aef5bc15c00ed9c05e33 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 19:50:40 +0000 Subject: [PATCH 20/35] fix(duckdb,fixtures): use range nextval loop for sequence resynchronization --- sqlspec/utils/fixtures.py | 16 +++++++++++++++- tests/unit/utils/test_fixtures.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/sqlspec/utils/fixtures.py b/sqlspec/utils/fixtures.py index d7ce6a2d4..0aeafe0ac 100644 --- a/sqlspec/utils/fixtures.py +++ b/sqlspec/utils/fixtures.py @@ -1898,7 +1898,7 @@ def _sequence_resync_statements( statements.append((statement, {"table_name": quoted_table, "column_name": column.name})) return statements - if dialect_name in {"cockroach", "cockroachdb", "duckdb"} or family in {"cockroach", "cockroachdb", "duckdb"}: + if dialect_name in {"cockroach", "cockroachdb"} or family in {"cockroach", "cockroachdb"}: for column in table_columns: if column.sequence_name is None: continue @@ -1910,6 +1910,20 @@ def _sequence_resync_statements( statements.append((statement, {"sequence_name": column.sequence_name})) return statements + if dialect_name == "duckdb" or family == "duckdb": + for column in table_columns: + if column.sequence_name is None: + continue + quoted_column = _quoted_identifier_sql(column.name) + statement = ( + f"SELECT count(nextval('{column.sequence_name}')) FROM range(" + f"(SELECT coalesce(last_value, start_value - increment_by) FROM duckdb_sequences() " + f"WHERE sequence_name = :sequence_name), " + f"(SELECT coalesce(max({quoted_column}), 0) FROM {_quoted_table_name(table)}))" + ) + statements.append((statement, {"sequence_name": column.sequence_name})) + return statements + if family == "oracle": for column in table_columns: if column.is_identity: diff --git a/tests/unit/utils/test_fixtures.py b/tests/unit/utils/test_fixtures.py index 3ecb5cea0..42a360c48 100644 --- a/tests/unit/utils/test_fixtures.py +++ b/tests/unit/utils/test_fixtures.py @@ -1824,7 +1824,7 @@ def test_sequence_resync_statements_cross_dialect() -> None: "users", [col_seq], dialect_name="duckdb", family="duckdb" ) assert len(stmts_duckdb) == 1 - assert 'setval(:sequence_name, coalesce(max("id"), 1)' in stmts_duckdb[0][0] + assert "count(nextval(" in stmts_duckdb[0][0] assert stmts_duckdb[0][1] == {"sequence_name": "users_id_seq"} stmts_tsql = fixture_module._sequence_resync_statements("users", [col_ident], dialect_name="tsql", family="tsql") From b99fdecc17d43289fff868f51b9a7ac516d73230 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 21:09:35 +0000 Subject: [PATCH 21/35] fix(duckdb,tests): restore literal destination URI for native copy statement --- .../adapters/duckdb/duckdb/test_native_storage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py index 3e317a38c..9c38480df 100644 --- a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py +++ b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py @@ -204,13 +204,13 @@ def test_arrow_and_native_copy_replace_single_local_object( assert job.telemetry["bytes_processed"] > 0 assert job.telemetry["destination"] == key copy_sql = ( - "COPY (SELECT ? AS value) TO ? (FORMAT PARQUET)" + f"COPY (SELECT ? AS value) TO '{uri}' (FORMAT PARQUET)" if file_format == "parquet" - else "COPY (SELECT ? AS value) TO ? (FORMAT CSV, HEADER TRUE)" + else f"COPY (SELECT ? AS value) TO '{uri}' (FORMAT CSV, HEADER TRUE)" ) read_sql = "SELECT * FROM read_parquet(?)" if file_format == "parquet" else "SELECT * FROM read_csv(?)" for value in [3, 4]: - assert session.connection.execute(copy_sql, [value, uri]).fetchone() == (1,) + assert session.connection.execute(copy_sql, [value]).fetchone() == (1,) assert session.connection.execute(read_sql, [uri]).fetchall() == [(value,)] finally: config.close_pool() From cfc18aa9430ae5d1dc219e194e252e2e3b00eef8 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 00:13:03 +0000 Subject: [PATCH 22/35] fix(testing): overhaul clientless test infrastructure and database sharding - register integration database plugins dynamically inside pytest_configure - update DuckDB sequence resynchronization to use CTE and schema-qualified range calculation - add shared apply_wallet_ssl_context helper for OracleDB with config_dir fallback and unit tests - rewrite Db2 ADBC non-query statement classification using public SQL statement properties - clean up TypedDict docstrings for AlloyDB columnar scan and drop unsupported psqlpy pool param - decouple ODBC DSN assertion from pinning SQLite fallback - centralize module resolution in tests/fixtures/modules.py across test conftests - separate collection hook tests into test_collection_groups.py and remove legacy test path guards - rename MySQL ADK residual test suite to test_adk_store.py and narrow FK integrity check - update Makefile integration and coverage targets to execute database shards sequentially - adjust CI integration job timeout to 45 minutes with a 35-minute watchdog - upgrade dependencies via make upgrade and point pytest-databases to test/clientless-all branch --- .github/workflows/ci.yml | 10 +- .pre-commit-config.yaml | 2 +- Makefile | 19 +- pyproject.toml | 2 + sqlspec/adapters/adbc/driver.py | 14 +- sqlspec/adapters/asyncpg/config.py | 2 +- sqlspec/adapters/oracledb/config.py | 12 +- sqlspec/adapters/oracledb/core.py | 14 + sqlspec/adapters/psqlpy/config.py | 4 +- sqlspec/adapters/psycopg/config.py | 2 +- sqlspec/utils/fixtures.py | 31 +- tests/conftest.py | 12 +- tests/fixtures/modules.py | 15 + tests/integration/adapters/README.md | 3 - .../_shared/suite_case_metadata_contract.py | 205 -- ...est_adk_residuals.py => test_adk_store.py} | 71 +- .../adapters/test_collection_groups.py | 162 + tests/integration/conftest.py | 19 +- tests/integration/fixtures/mysql.py | 14 +- .../test_adbc_config_normalization.py | 3 +- .../test_core_driver_features.py | 31 +- tests/unit/test_import_time.py | 25 +- tests/unit/utils/test_fixtures.py | 34 +- uv.lock | 3074 ++++++++--------- 24 files changed, 1742 insertions(+), 2038 deletions(-) create mode 100644 tests/fixtures/modules.py rename tests/integration/adapters/mysql/{test_adk_residuals.py => test_adk_store.py} (63%) create mode 100644 tests/integration/adapters/test_collection_groups.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f708cd2d..c4cfe0727 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,7 +260,7 @@ jobs: -n auto --dist=loadgroup -p no:cacheprovider test-integration: - name: "Integration (Py ${{ matrix.python-version }}, ${{ matrix.shard }})" + name: "test-integration (${{ matrix.python-version }}, ${{ matrix.shard }})" needs: [ci-scope, test-unit] if: needs.ci-scope.outputs.ci_required == 'true' runs-on: ubuntu-24.04 @@ -301,7 +301,7 @@ jobs: images: "ghcr.io/goccy/bigquery-emulator:latest gcr.io/cloud-spanner-emulator/emulator:latest gizmodata/gizmosql:latest rustfs/rustfs:latest rustfs/rc:latest floci/floci-gcp:latest floci/floci:latest mcr.microsoft.com/azure-storage/azurite:latest embucket/embucket:latest" pytest_targets: "tests/integration/adapters/bigquery tests/integration/adapters/spanner tests/integration/adapters/gizmosql tests/integration/storage tests/integration/adapters/snowflake" extra_pytest_flags: "--run-bigquery-tests --run-spanner-tests" - timeout-minutes: 30 + timeout-minutes: 45 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -481,9 +481,9 @@ jobs: pytest_pgid=$pytest_pid ( - sleep 1320 + sleep 2100 if kill -0 -"$pytest_pgid" 2>/dev/null; then - echo "::group::Hang detected after 22 minutes - SIGABRT to pytest process group $pytest_pgid" + echo "::group::Hang detected after 35 minutes - SIGABRT to pytest process group $pytest_pgid" kill -ABRT -"$pytest_pgid" 2>/dev/null || true sleep 15 if kill -0 -"$pytest_pgid" 2>/dev/null; then @@ -511,7 +511,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Dump python stacks on hang - if: failure() && steps.pytest.outcome == 'failure' + if: always() && steps.pytest.outcome == 'failure' run: | pip install --quiet py-spy || true for pid in $(pgrep -f 'pytest|python' || true); do diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe0a27f2d..79e0bf1bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.17.1" + rev: "v0.17.2" hooks: - id: ruff-check args: ["--fix"] diff --git a/Makefile b/Makefile index 07bdc4045..4c070025d 100644 --- a/Makefile +++ b/Makefile @@ -173,10 +173,7 @@ clean: ## Cleanup temporary build a # ============================================================================= .PHONY: test -test: ## Run the tests - @echo "${INFO} Running test cases... 🧪" - @uv run pytest -n $(UNIT_WORKERS) --dist=loadgroup tests/unit - @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup tests/integration +test: test-unit test-integration ## Run the tests @echo "${OK} Tests complete ✨" .PHONY: test-unit @@ -186,9 +183,7 @@ test-unit: ## Run unit tests @echo "${OK} Unit tests complete ✨" .PHONY: test-integration -test-integration: ## Run integration tests - @echo "${INFO} Running integration tests... 🧪" - @uv run pytest -n $(INTEGRATION_WORKERS) --dist=loadgroup tests/integration +test-integration: test-integration-postgres-cockroach test-integration-mysql-mariadb-embedded test-integration-oracle test-integration-mssql-db2 test-integration-cloud-adbc-storage ## Run integration tests shard by shard @echo "${OK} Integration tests complete ✨" .PHONY: test-integration-postgres-cockroach @@ -232,7 +227,15 @@ coverage: ## Run tests with coverage r @uv run pytest --cov --cov-report= --cov-fail-under=0 -n $(UNIT_WORKERS) --dist=loadgroup --quiet \ tests/unit @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --quiet \ - tests/integration + $(SHARD_POSTGRES_COCKROACH) + @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --quiet \ + $(SHARD_MYSQL_MARIADB_EMBEDDED) + @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --quiet \ + $(SHARD_ORACLE) + @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --quiet \ + $(SHARD_MSSQL_DB2) + @uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n $(INTEGRATION_WORKERS) --dist=loadgroup --run-bigquery-tests --run-spanner-tests --quiet \ + $(SHARD_CLOUD_ADBC_STORAGE) @uv run coverage report --fail-under=0 @uv run coverage html >/dev/null 2>&1 @uv run coverage xml >/dev/null 2>&1 diff --git a/pyproject.toml b/pyproject.toml index d6b5c1799..7737cd7f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,8 +184,10 @@ litestar-queues = false litestar-saq = false litestar-security = false litestar-vite = false +mypy-extensions = false polyfactory = false pytest-databases = false +rich-click = false sqlglot = false sqlglotc = false diff --git a/sqlspec/adapters/adbc/driver.py b/sqlspec/adapters/adbc/driver.py index e03ccdfbd..533168b5b 100644 --- a/sqlspec/adapters/adbc/driver.py +++ b/sqlspec/adapters/adbc/driver.py @@ -9,8 +9,6 @@ from time import perf_counter from typing import TYPE_CHECKING, Any, Literal, cast, final -import sqlglot - from sqlspec.adapters.adbc._typing import AdbcCursor, AdbcNativeError, AdbcSessionContext from sqlspec.adapters.adbc.core import ( apply_driver_features, @@ -47,7 +45,6 @@ get_cache_config, register_driver_profile, ) -from sqlspec.core.compiler import SQLProcessor from sqlspec.core.result import DMLResult from sqlspec.driver import BaseSyncExceptionHandler, SyncDriverAdapterBase, SyncRowStream, validate_savepoint_name from sqlspec.exceptions import DatabaseConnectionError, SQLSpecError @@ -60,8 +57,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from sqlglot import exp - from sqlspec.adapters.adbc._typing import AdbcConnection, AdbcRawCursor from sqlspec.builder import QueryBuilder from sqlspec.core import ArrowResult, SQLResult, Statement, StatementFilter @@ -955,13 +950,8 @@ def _is_db2_non_query_statement(self, sql: str, *, parent_statement: "SQL", tota """Return whether a script statement is a non-row-returning Db2 operation.""" if total_statements == 1: return not parent_statement.returns_rows() and parent_statement.operation_type != "COMMAND" - expression: exp.Expression | None = sqlglot.maybe_parse(sql, dialect="db2") - if expression is None: - return False - operation_type = SQLProcessor._operation_type(expression) - if operation_type == "COMMAND": - return False - return not SQLProcessor._operation_profile(expression, operation_type).returns_rows + statement = SQL(sql, statement_config=self.statement_config) + return not statement.returns_rows() and statement.operation_type != "COMMAND" @staticmethod def _execute_db2_non_query(cursor: "AdbcRawCursor", sql: str, execute_parameters: Any) -> None: diff --git a/sqlspec/adapters/asyncpg/config.py b/sqlspec/adapters/asyncpg/config.py index 47c817478..50764a004 100644 --- a/sqlspec/adapters/asyncpg/config.py +++ b/sqlspec/adapters/asyncpg/config.py @@ -197,7 +197,7 @@ class AsyncpgDriverFeatures(TypedDict): alloydb_ip_type: NotRequired[str] """AlloyDB network path (``PUBLIC``, ``PRIVATE``, or ``PSC``; defaults to ``PRIVATE``).""" enable_alloydb_columnar: NotRequired[bool] - """Enable ``google_columnar_engine.enabled=on`` at session startup or per transaction.""" + """Enable ``google_columnar_engine.enable_columnar_scan=on`` at session startup or per transaction.""" enable_events: NotRequired[bool] """Enable SQLSpec event channel pub/sub support (defaults to ``True`` when ``events`` is configured).""" events_backend: NotRequired[Literal["notify", "notify_queue", "poll_queue"]] diff --git a/sqlspec/adapters/oracledb/config.py b/sqlspec/adapters/oracledb/config.py index a088989ab..150678834 100644 --- a/sqlspec/adapters/oracledb/config.py +++ b/sqlspec/adapters/oracledb/config.py @@ -30,8 +30,8 @@ OracleSessionContextConfig, OracleTopologyConfig, apply_driver_features, + apply_wallet_ssl_context, build_connection_config, - build_wallet_ssl_context_if_needed, default_statement_config, execute_session_checkout_async, execute_session_checkout_sync, @@ -587,10 +587,7 @@ def _create_pool(self) -> "OracleSyncConnectionPool": client_config["config_dir"] = config["config_dir"] oracledb.init_oracle_client(**client_config) - if oracledb.is_thin_mode() and "ssl_context" not in config: - wallet_ssl_ctx = build_wallet_ssl_context_if_needed(config.get("wallet_location")) - if wallet_ssl_ctx is not None: - config["ssl_context"] = wallet_ssl_ctx + apply_wallet_ssl_context(config) config.pop("threaded", None) config["session_callback"] = self._init_connection @@ -910,10 +907,7 @@ async def _create_pool(self) -> "OracleAsyncConnectionPool": msg = "OracleAsyncConfig only supports Thin mode; use OracleSyncConfig for Thick mode." raise ImproperConfigurationError(msg) - if oracledb.is_thin_mode() and "ssl_context" not in config: - wallet_ssl_ctx = build_wallet_ssl_context_if_needed(config.get("wallet_location")) - if wallet_ssl_ctx is not None: - config["ssl_context"] = wallet_ssl_ctx + apply_wallet_ssl_context(config) config.pop("threaded", None) config["session_callback"] = self._init_connection diff --git a/sqlspec/adapters/oracledb/core.py b/sqlspec/adapters/oracledb/core.py index a71a32f30..fa803ab4c 100644 --- a/sqlspec/adapters/oracledb/core.py +++ b/sqlspec/adapters/oracledb/core.py @@ -97,6 +97,7 @@ "OracleTopologyConfig", "apply_driver_features", "apply_oracle_session_trace_attributes", + "apply_wallet_ssl_context", "build_arrow_fetch_kwargs", "build_connection_config", "build_dbms_cloud_copy_data_sql", @@ -2426,6 +2427,19 @@ def build_dbms_cloud_copy_data_sql( return plsql, binds +def apply_wallet_ssl_context(config: dict[str, Any]) -> None: + """Apply an SSLContext for Oracle wallets with non-strict self-signed CAs if needed. + + Args: + config: Connection configuration dictionary. + """ + if oracledb_module.is_thin_mode() and "ssl_context" not in config: + wallet_loc = config.get("wallet_location") or config.get("config_dir") + wallet_ssl_ctx = build_wallet_ssl_context_if_needed(wallet_loc) + if wallet_ssl_ctx is not None: + config["ssl_context"] = wallet_ssl_ctx + + def build_wallet_ssl_context_if_needed(wallet_location: Any) -> "ssl.SSLContext | None": """Build a Thin-mode ``ssl.SSLContext`` for Oracle wallets with non-strict self-signed CAs. diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py index 84682ba78..ff758887d 100644 --- a/sqlspec/adapters/psqlpy/config.py +++ b/sqlspec/adapters/psqlpy/config.py @@ -124,8 +124,6 @@ class PsqlpyPoolParams(PsqlpyConnectionParams): """Pool connection reset strategy (such as ``fast`` or ``verified``).""" max_db_pool_size: NotRequired[int] """Maximum number of concurrent connections held by ``psqlpy.ConnectionPool``.""" - max_cached_statement_lifetime: NotRequired[int] - """Maximum lifetime in seconds for cached prepared statements.""" configure: NotRequired["Callable[..., Any]"] """Optional pool connection configuration callback.""" extra: NotRequired["dict[str, Any]"] @@ -144,7 +142,7 @@ class PsqlpyDriverFeatures(TypedDict): enable_pg_textsearch: NotRequired[bool] """Detect ``pg_textsearch`` and promote the dialect for BM25 ``<@>`` ranking (defaults to ``True``).""" enable_alloydb_columnar: NotRequired[bool] - """Enable ``google_columnar_engine.enabled=on`` at session startup or per transaction.""" + """Enable ``google_columnar_engine.enable_columnar_scan=on`` at session startup or per transaction.""" pooler_mode: NotRequired[Literal["session", "transaction", "statement", "auto"] | None] """External pooler compatibility mode; executes queries with ``prepared=False`` when not ``session``.""" pgbouncer: NotRequired[bool] diff --git a/sqlspec/adapters/psycopg/config.py b/sqlspec/adapters/psycopg/config.py index 8643fb419..42b829044 100644 --- a/sqlspec/adapters/psycopg/config.py +++ b/sqlspec/adapters/psycopg/config.py @@ -215,7 +215,7 @@ class PsycopgDriverFeatures(TypedDict): alloydb_ip_type: NotRequired[str] """AlloyDB network path (``PUBLIC``, ``PRIVATE``, or ``PSC``; defaults to ``PRIVATE``).""" enable_alloydb_columnar: NotRequired[bool] - """Enable ``google_columnar_engine.enabled=on`` via startup ``options`` or per-transaction ``SET LOCAL``.""" + """Enable ``google_columnar_engine.enable_columnar_scan=on`` via startup ``options`` or per-transaction ``SET LOCAL``.""" pooler_mode: NotRequired[Literal["session", "transaction", "statement", "auto"]] """External pooler compatibility mode; sets ``prepare_threshold=None`` in ``transaction`` or ``statement`` mode.""" pgbouncer: NotRequired[bool] diff --git a/sqlspec/utils/fixtures.py b/sqlspec/utils/fixtures.py index 0aeafe0ac..17b834f42 100644 --- a/sqlspec/utils/fixtures.py +++ b/sqlspec/utils/fixtures.py @@ -1915,13 +1915,34 @@ def _sequence_resync_statements( if column.sequence_name is None: continue quoted_column = _quoted_identifier_sql(column.name) + sequence_schema, _, sequence_name = column.sequence_name.rpartition(".") + if not sequence_schema: + table_schema, _, _ = table.rpartition(".") + sequence_schema = table_schema or "main" + quoted_seq = f"{sequence_schema}.{sequence_name}" if sequence_schema else sequence_name statement = ( - f"SELECT count(nextval('{column.sequence_name}')) FROM range(" - f"(SELECT coalesce(last_value, start_value - increment_by) FROM duckdb_sequences() " - f"WHERE sequence_name = :sequence_name), " - f"(SELECT coalesce(max({quoted_column}), 0) FROM {_quoted_table_name(table)}))" + f"WITH seq_info AS (" + f"SELECT coalesce(last_value, start_value - increment_by) AS cur_val, increment_by " + f"FROM duckdb_sequences() " + f"WHERE schema_name = :sequence_schema AND sequence_name = :sequence_name" + f"), " + f"table_max AS (" + f"SELECT coalesce(max({quoted_column}), 0) AS max_val FROM {_quoted_table_name(table)}" + f"), " + f"calc AS (" + f"SELECT CASE " + f"WHEN table_max.max_val > seq_info.cur_val AND seq_info.increment_by > 0 " + f"THEN cast(ceil((table_max.max_val - seq_info.cur_val) * 1.0 / seq_info.increment_by) AS bigint) " + f"ELSE 0 " + f"END AS steps " + f"FROM seq_info CROSS JOIN table_max" + f") " + f"SELECT count(nextval(:quoted_seq)) FROM calc, range(calc.steps)" ) - statements.append((statement, {"sequence_name": column.sequence_name})) + statements.append(( + statement, + {"sequence_schema": sequence_schema, "sequence_name": sequence_name, "quoted_seq": quoted_seq}, + )) return statements if family == "oracle": diff --git a/tests/conftest.py b/tests/conftest.py index b413869da..82a709a25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,16 @@ """Root pytest configuration and shared test hooks.""" -import functools import importlib.machinery import logging import os import warnings from collections.abc import Generator -from importlib.util import find_spec from pathlib import Path import pytest from sqlspec.core.cache import clear_all_caches +from tests.fixtures.modules import module_is_available as _module_is_available warnings.filterwarnings( "ignore", message="You are using a Python version.*which Google will stop supporting", category=FutureWarning @@ -175,15 +174,6 @@ def clear_sql_caches() -> Generator[None, None, None]: clear_all_caches() -@functools.cache -def _module_is_available(module: str) -> bool: - """Return True when a module specification can be resolved without importing it.""" - try: - return find_spec(module) is not None - except ModuleNotFoundError: - return False - - def _env_flag_enabled(name: str) -> bool: """Return True when an environment variable holds a truthy opt-in value.""" return os.getenv(name, "").strip().lower() in _TRUTHY_ENV_VALUES diff --git a/tests/fixtures/modules.py b/tests/fixtures/modules.py new file mode 100644 index 000000000..5d0c399bd --- /dev/null +++ b/tests/fixtures/modules.py @@ -0,0 +1,15 @@ +"""Module resolution helpers for tests.""" + +import functools +from importlib.util import find_spec + +__all__ = ("module_is_available",) + + +@functools.cache +def module_is_available(module: str) -> bool: + """Return True when a module specification can be resolved without importing it.""" + try: + return find_spec(module) is not None + except ModuleNotFoundError: + return False diff --git a/tests/integration/adapters/README.md b/tests/integration/adapters/README.md index a73ad1e34..14b65aeac 100644 --- a/tests/integration/adapters/README.md +++ b/tests/integration/adapters/README.md @@ -264,6 +264,3 @@ group under their database family. BigQuery and Spanner emulator suites run automatically in CI (`CI=true`). Local runs opt in with either `--run-bigquery-tests` or `SQLSPEC_ENABLE_BIGQUERY_TESTS=1` for BigQuery, and either `--run-spanner-tests` or `SQLSPEC_ENABLE_SPANNER_TESTS=1` for Spanner. - -`suite_case_metadata_contract.py` includes a no-orphan guard (`CONTRACT_OWNED_REMOVED_TEST_PATHS`) -for files removed by consolidation, so shared behavior cannot silently reappear in driver-local files. diff --git a/tests/integration/adapters/_shared/suite_case_metadata_contract.py b/tests/integration/adapters/_shared/suite_case_metadata_contract.py index 6d3d1b604..88be209ce 100644 --- a/tests/integration/adapters/_shared/suite_case_metadata_contract.py +++ b/tests/integration/adapters/_shared/suite_case_metadata_contract.py @@ -1,10 +1,7 @@ """Contract metadata guardrails for adapter case records.""" -from pathlib import Path from typing import cast -import msgspec -import pytest from _pytest.mark.structures import ParameterSet from tests.integration.adapters._shared._adk_cases import AdkStoreCase, adk_store_params_with @@ -28,7 +25,6 @@ ASYNC_POSTGRES_EXTENSION_CASES, SYNC_POSTGRES_EXTENSION_CASES, ) -from tests.integration.adapters.conftest import _case_family, pytest_collection_modifyitems MYSQL_ROW_LOCKING_CASE_IDS = { "aiomysql-async", @@ -98,51 +94,6 @@ "psycopg-paradedb-sync", "psycopg-pgvector-sync", } -CONTRACT_OWNED_REMOVED_TEST_PATHS = { - "adbc/test_adbc_results.py", - "adbc/test_data_dictionary.py", - "adbc/test_exceptions.py", - "adbc/test_migrations.py", - "adbc/test_paradedb.py", - "adbc/test_pgvector.py", - "adbc/test_vector_functions.py", - "aiomysql/test_storage_bridge.py", - "aiosqlite/test_data_dictionary.py", - "asyncmy/test_storage_bridge.py", - "asyncpg/extensions/events/test_queue_backend.py", - "asyncpg/test_data_dictionary.py", - "asyncpg/test_merge.py", - "asyncpg/test_merge_bulk.py", - "asyncpg/test_paradedb.py", - "asyncpg/test_pgvector.py", - "bigquery/test_exceptions.py", - "duckdb/test_migrations.py", - "duckdb/test_vector_functions.py", - "mysql/aiomysql/extensions/adk/test_store.py", - "mysql/asyncmy/extensions/adk/test_store.py", - "mysql/mysqlconnector/extensions/adk/test_store.py", - "mysqlconnector/test_driver_async.py", - "oracledb/extensions/events/test_queue_backend.py", - "oracledb/extensions/litestar/test_store_async.py", - "oracledb/extensions/litestar/test_store_sync.py", - "oracledb/test_exceptions.py", - "oracledb/test_merge.py", - "oracledb/test_merge_bulk.py", - "oracledb/test_stack.py", - "oracledb/test_vector_functions.py", - "postgres/test_cockroach_retry.py", - "psqlpy/test_data_dictionary.py", - "psqlpy/test_merge_bulk.py", - "psqlpy/test_migrations.py", - "psqlpy/test_paradedb.py", - "psqlpy/test_pgvector.py", - "psycopg/test_data_dictionary.py", - "psycopg/test_merge_bulk.py", - "psycopg/test_migrations.py", - "psycopg/test_paradedb.py", - "psycopg/test_pgvector.py", - "sqlite/test_data_dictionary.py", -} def test_active_driver_cases_do_not_use_string_deviations() -> None: @@ -266,14 +217,6 @@ def test_postgres_extension_cases_are_contract_owned() -> None: assert case_ids == POSTGRES_EXTENSION_CASE_IDS -def test_contract_owned_adapter_local_test_files_do_not_reappear() -> None: - """Adapter-local files removed by consolidation stay contract-owned.""" - assert all(not path.startswith("tests/") for path in CONTRACT_OWNED_REMOVED_TEST_PATHS) - adapters_root = Path(__file__).resolve().parents[1] - existing = sorted(path for path in CONTRACT_OWNED_REMOVED_TEST_PATHS if (adapters_root / path).exists()) - assert existing == [] - - def test_adk_capability_params_match_requested_capability() -> None: """ADK capability-filtered params collect only cases that declare the requested capability.""" cases = _adk_cases(adk_store_params_with("supports_atomic_state_update")) @@ -292,154 +235,6 @@ def test_parameter_style_params_are_method_filtered() -> None: } -def test_case_family_routes_adbc_dialects() -> None: - """ADBC case identifiers route to their backing database family instead of defaulting to postgres.""" - expected_routes = { - "adbc-oracle": "oracle", - "adbc-db2": "db2", - "adbc-snowflake": "snowflake", - "adbc-bigquery": "bigquery", - "adbc-duckdb": "duckdb", - "adbc-sqlite": "sqlite", - "adbc-postgres": "postgres", - "adbc-pgvector": "postgres", - "adbc-gizmosql-duckdb": "gizmosql", - } - observed_routes = {case_id: _case_family(_StubCaseId(id=case_id)) for case_id in expected_routes} - assert observed_routes == expected_routes - - -def test_collection_preserves_container_distinct_xdist_groups( - request: pytest.FixtureRequest, pytestconfig: pytest.Config -) -> None: - """Container-distinct xdist_group markers survive multi-level collector normalization without compound leakage.""" - adapters_root = Path(__file__).resolve().parents[1] - preserved_groups = ( - ("postgres", "alloydb", False), - ("gizmosql", "gizmosql_sqlite", True), - ("mysql", "mariadb", True), - ("oracle", "oracle_18c", False), - ("oracle", "oracle_adb", True), - ("postgres", "paradedb", True), - ("postgres", "pgvector", False), - ("snowflake", "snowflake", False), - ) - items: list[pytest.Item] = [] - preserved_expectations: list[tuple[pytest.Item, str]] = [] - for family, group_name, use_keyword in preserved_groups: - family_pkg = _StubAdapterCollector.from_parent( - request.session, name=family, path=adapters_root / family / "__init__.py" - ) - family_pkg.add_marker(pytest.mark.xdist_group("cockroachdb" if family == "cockroach" else family)) - module_collector = _StubAdapterCollector.from_parent( - family_pkg, name=f"test_{group_name}.py", path=adapters_root / family / f"test_{group_name}.py" - ) - group_marker = pytest.mark.xdist_group(name=group_name) if use_keyword else pytest.mark.xdist_group(group_name) - module_collector.add_marker(group_marker) - first_item = _StubAdapterItem.from_parent( - module_collector, name=f"test_{group_name}_first", path=module_collector.path - ) - second_item = _StubAdapterItem.from_parent( - module_collector, name=f"test_{group_name}_second", path=module_collector.path - ) - items.extend((first_item, second_item)) - preserved_expectations.extend(((first_item, group_name), (second_item, group_name))) - - oracle_pkg = _StubAdapterCollector.from_parent( - request.session, name="oracledb", path=adapters_root / "oracle" / "oracledb" / "__init__.py" - ) - oracle_pkg.add_marker(pytest.mark.xdist_group("oracle")) - oracle_json_mod = _StubAdapterCollector.from_parent( - oracle_pkg, - name="test_json_storage_modes.py", - path=adapters_root / "oracle" / "oracledb" / "test_json_storage_modes.py", - ) - oracle_json_mod.add_marker(pytest.mark.xdist_group("oracle")) - default_oracle_item = _StubAdapterItem.from_parent( - oracle_json_mod, name="test_native_json_round_trip_matrix", path=oracle_json_mod.path - ) - oracle_18c_item = _StubAdapterItem.from_parent( - oracle_json_mod, name="test_blob_is_json_round_trip_on_oracle_18c", path=oracle_json_mod.path - ) - oracle_18c_item.add_marker(pytest.mark.xdist_group("oracle_18c")) - oracle_adb_cls = _StubAdapterCollector.from_parent( - oracle_json_mod, name="TestOracleAdbWallet", path=oracle_json_mod.path - ) - oracle_adb_cls.add_marker(pytest.mark.xdist_group(name="oracle_adb")) - oracle_adb_class_item = _StubAdapterItem.from_parent( - oracle_adb_cls, name="test_wallet_mtls", path=oracle_json_mod.path - ) - - cloud_mod = _StubAdapterCollector.from_parent( - request.session, - name="test_cloud_connectors.py", - path=adapters_root / "postgres" / "asyncpg" / "test_cloud_connectors.py", - ) - cloud_mod.add_marker(pytest.mark.xdist_group("google_cloud")) - cloud_postgres_item = _StubAdapterItem.from_parent(cloud_mod, name="test_cloud_sql_connector", path=cloud_mod.path) - - ungrouped_postgres = _StubAdapterItem.from_parent( - request.session, name="test_ungrouped_postgres", path=adapters_root / "postgres" / "test_driver.py" - ) - overridden_postgres = _StubAdapterItem.from_parent( - request.session, name="test_overridden_postgres", path=adapters_root / "postgres" / "test_driver.py" - ) - overridden_postgres.add_marker(pytest.mark.xdist_group(name="custom_transient_group")) - ungrouped_cockroach = _StubAdapterItem.from_parent( - request.session, name="test_ungrouped_cockroach", path=adapters_root / "cockroach" / "test_retry.py" - ) - ungrouped_snowflake = _StubAdapterItem.from_parent( - request.session, name="test_ungrouped_snowflake", path=adapters_root / "snowflake" / "test_adbc.py" - ) - items.extend(( - default_oracle_item, - oracle_18c_item, - oracle_adb_class_item, - cloud_postgres_item, - ungrouped_postgres, - overridden_postgres, - ungrouped_cockroach, - ungrouped_snowflake, - )) - - pytest_collection_modifyitems(pytestconfig, items) - - for item, expected_group in preserved_expectations: - assert _xdist_groups(item) == [expected_group] - assert _xdist_groups(default_oracle_item) == ["oracle"] - assert _xdist_groups(oracle_18c_item) == ["oracle_18c"] - assert _xdist_groups(oracle_adb_class_item) == ["oracle_adb"] - assert _xdist_groups(cloud_postgres_item) == ["postgres"] - assert _xdist_groups(ungrouped_postgres) == ["postgres"] - assert _xdist_groups(overridden_postgres) == ["postgres"] - assert _xdist_groups(ungrouped_cockroach) == ["cockroachdb"] - assert _xdist_groups(ungrouped_snowflake) == ["snowflake"] - - -class _StubCaseId(msgspec.Struct, frozen=True, gc=False): - id: str - - -class _StubAdapterCollector(pytest.Collector): - def collect(self) -> list[pytest.Item | pytest.Collector]: - return [] - - -class _StubAdapterItem(pytest.Item): - def runtest(self) -> None: - return None - - -def _xdist_groups(item: pytest.Item) -> list[str]: - groups: list[str] = [] - for mark in item.iter_markers(name="xdist_group"): - if mark.args: - groups.append(str(mark.args[0])) - elif (name := mark.kwargs.get("name")) is not None: - groups.append(str(name)) - return groups - - def _driver_cases(params: tuple[ParameterSet, ...]) -> tuple[DriverCase, ...]: return tuple(cast("DriverCase", param.values[0]) for param in params) diff --git a/tests/integration/adapters/mysql/test_adk_residuals.py b/tests/integration/adapters/mysql/test_adk_store.py similarity index 63% rename from tests/integration/adapters/mysql/test_adk_residuals.py rename to tests/integration/adapters/mysql/test_adk_store.py index 3654c7972..41ba6344b 100644 --- a/tests/integration/adapters/mysql/test_adk_residuals.py +++ b/tests/integration/adapters/mysql/test_adk_store.py @@ -1,15 +1,16 @@ -"""Unified MySQL ADK residual integration tests across all MySQL driver modes.""" +"""Unified MySQL ADK store integration tests across all MySQL driver modes.""" from datetime import UTC, datetime from typing import TYPE_CHECKING import pytest +from sqlspec.exceptions import IntegrityError from sqlspec.utils.module_loader import import_optional_attr if TYPE_CHECKING: from sqlspec.extensions.adk._types import StoredEvent - from tests.integration.fixtures.mysql import MysqlAdkResidualContext + from tests.integration.fixtures.mysql import MysqlAdkStoreHarness else: StoredEvent = import_optional_attr("sqlspec.extensions.adk._types", "StoredEvent") @@ -18,12 +19,10 @@ pytestmark = [pytest.mark.mysql, pytest.mark.integration, pytest.mark.xdist_group("mysql")] -async def test_mysql_adk_json_and_timestamp6_column_types( - mysql_adk_residual_context: "MysqlAdkResidualContext", -) -> None: +async def test_mysql_adk_json_and_timestamp6_column_types(mysql_adk_store_harness: "MysqlAdkStoreHarness") -> None: """Verify MySQL ADK tables use native JSON columns and TIMESTAMP(6) microsecond columns.""" - session_columns = await mysql_adk_residual_context.select_rows( - mysql_adk_residual_context.config, + session_columns = await mysql_adk_store_harness.select_rows( + mysql_adk_store_harness.config, """ SELECT COLUMN_NAME AS column_name, @@ -35,7 +34,7 @@ async def test_mysql_adk_json_and_timestamp6_column_types( AND TABLE_NAME = :table_name ORDER BY ORDINAL_POSITION """, - table_name=mysql_adk_residual_context.session_table, + table_name=mysql_adk_store_harness.session_table, ) by_session_col = {str(row["column_name"]): row for row in session_columns} assert str(by_session_col["state"]["data_type"]).lower() == "json" @@ -44,8 +43,8 @@ async def test_mysql_adk_json_and_timestamp6_column_types( assert "timestamp(6)" in str(by_session_col["update_time"]["column_type"]).lower() assert int(by_session_col["update_time"]["datetime_precision"]) == 6 - event_columns = await mysql_adk_residual_context.select_rows( - mysql_adk_residual_context.config, + event_columns = await mysql_adk_store_harness.select_rows( + mysql_adk_store_harness.config, """ SELECT COLUMN_NAME AS column_name, @@ -57,7 +56,7 @@ async def test_mysql_adk_json_and_timestamp6_column_types( AND TABLE_NAME = :table_name ORDER BY ORDINAL_POSITION """, - table_name=mysql_adk_residual_context.events_table, + table_name=mysql_adk_store_harness.events_table, ) by_event_col = {str(row["column_name"]): row for row in event_columns} assert {"session_id", "invocation_id", "timestamp", "event_data"}.issubset(by_event_col) @@ -66,14 +65,14 @@ async def test_mysql_adk_json_and_timestamp6_column_types( assert int(by_event_col["timestamp"]["datetime_precision"]) == 6 -async def test_mysql_adk_microsecond_timestamp_roundtrip(mysql_adk_residual_context: "MysqlAdkResidualContext") -> None: +async def test_mysql_adk_microsecond_timestamp_roundtrip(mysql_adk_store_harness: "MysqlAdkStoreHarness") -> None: """Verify TIMESTAMP(6) preserves sub-second microsecond precision on sessions and events.""" session_id = "session-micro-001" app_name = "test-app" user_id = "user-micro-001" - created = await mysql_adk_residual_context.create_session( - mysql_adk_residual_context.store, session_id, app_name, user_id, {"precision": "microsecond"} + created = await mysql_adk_store_harness.create_session( + mysql_adk_store_harness.store, session_id, app_name, user_id, {"precision": "microsecond"} ) assert isinstance(created["create_time"], datetime) @@ -87,26 +86,24 @@ async def test_mysql_adk_microsecond_timestamp_roundtrip(mysql_adk_residual_cont "timestamp": event_time, "event_data": {"app_name": app_name, "author": "system"}, } - await mysql_adk_residual_context.append_event(mysql_adk_residual_context.store, event) + await mysql_adk_store_harness.append_event(mysql_adk_store_harness.store, event) - events = await mysql_adk_residual_context.get_events( - mysql_adk_residual_context.store, app_name, user_id, session_id - ) + events = await mysql_adk_store_harness.get_events(mysql_adk_store_harness.store, app_name, user_id, session_id) assert len(events) == 1 assert events[0]["timestamp"].microsecond == 123456 async def test_mysql_adk_owner_id_fk_constraint_isolation_and_cascade( - mysql_adk_residual_context: "MysqlAdkResidualContext", + mysql_adk_store_harness: "MysqlAdkStoreHarness", ) -> None: """Verify owner_id_column DDL, FK referential integrity, multi-tenant isolation, and ON DELETE CASCADE.""" - fk_store = mysql_adk_residual_context.fk_store - fk_config = mysql_adk_residual_context.fk_config + fk_store = mysql_adk_store_harness.fk_store + fk_config = mysql_adk_store_harness.fk_config assert fk_store.owner_id_column_name == "tenant_id" assert fk_store.owner_id_column_ddl is not None assert "tenant_id" in fk_store.owner_id_column_ddl - tenant_col_rows = await mysql_adk_residual_context.select_rows( + tenant_col_rows = await mysql_adk_store_harness.select_rows( fk_config, """ SELECT COLUMN_NAME AS column_name, DATA_TYPE AS data_type @@ -115,45 +112,45 @@ async def test_mysql_adk_owner_id_fk_constraint_isolation_and_cascade( AND TABLE_NAME = :table_name AND COLUMN_NAME = 'tenant_id' """, - table_name=mysql_adk_residual_context.fk_session_table, + table_name=mysql_adk_store_harness.fk_session_table, ) assert len(tenant_col_rows) == 1 assert str(tenant_col_rows[0]["data_type"]).lower() == "bigint" app_name = "test-app" user_id = "user-shared" - await mysql_adk_residual_context.create_session( + await mysql_adk_store_harness.create_session( fk_store, "tenant1-s1", app_name, user_id, {"tenant": "one"}, owner_id=1 ) - await mysql_adk_residual_context.create_session( + await mysql_adk_store_harness.create_session( fk_store, "tenant1-s2", app_name, user_id, {"tenant": "one"}, owner_id=1 ) - await mysql_adk_residual_context.create_session( + await mysql_adk_store_harness.create_session( fk_store, "tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2 ) - with pytest.raises(Exception): - await mysql_adk_residual_context.create_session( + with pytest.raises(IntegrityError): + await mysql_adk_store_harness.create_session( fk_store, "invalid-fk", app_name, user_id, {"tenant": "invalid"}, owner_id=999 ) - tenant1_rows = await mysql_adk_residual_context.select_rows( + tenant1_rows = await mysql_adk_store_harness.select_rows( fk_config, - f"SELECT id AS id FROM {mysql_adk_residual_context.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", + f"SELECT id AS id FROM {mysql_adk_store_harness.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", tenant_id=1, ) assert [row["id"] for row in tenant1_rows] == ["tenant1-s1", "tenant1-s2"] - tenant2_rows = await mysql_adk_residual_context.select_rows( + tenant2_rows = await mysql_adk_store_harness.select_rows( fk_config, - f"SELECT id AS id FROM {mysql_adk_residual_context.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", + f"SELECT id AS id FROM {mysql_adk_store_harness.fk_session_table} WHERE tenant_id = :tenant_id ORDER BY id", tenant_id=2, ) assert [row["id"] for row in tenant2_rows] == ["tenant2-s1"] - await mysql_adk_residual_context.execute_sql( - fk_config, f"DELETE FROM {mysql_adk_residual_context.tenants_table} WHERE id = 1" + await mysql_adk_store_harness.execute_sql( + fk_config, f"DELETE FROM {mysql_adk_store_harness.tenants_table} WHERE id = 1" ) - assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant1-s1") is None - assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant1-s2") is None - assert await mysql_adk_residual_context.get_session(fk_store, app_name, user_id, "tenant2-s1") is not None + assert await mysql_adk_store_harness.get_session(fk_store, app_name, user_id, "tenant1-s1") is None + assert await mysql_adk_store_harness.get_session(fk_store, app_name, user_id, "tenant1-s2") is None + assert await mysql_adk_store_harness.get_session(fk_store, app_name, user_id, "tenant2-s1") is not None diff --git a/tests/integration/adapters/test_collection_groups.py b/tests/integration/adapters/test_collection_groups.py new file mode 100644 index 000000000..0c5f206e4 --- /dev/null +++ b/tests/integration/adapters/test_collection_groups.py @@ -0,0 +1,162 @@ +"""Tests for database-family collection and xdist group assignment.""" + +from pathlib import Path + +import msgspec +import pytest + +from tests.integration.adapters.conftest import ( + _PRESERVED_CONTAINER_XDIST_GROUPS, + _case_family, + pytest_collection_modifyitems, +) + + +def test_case_family_routes_adbc_dialects() -> None: + """ADBC case identifiers route to their backing database family instead of defaulting to postgres.""" + expected_routes = { + "adbc-oracle": "oracle", + "adbc-db2": "db2", + "adbc-snowflake": "snowflake", + "adbc-bigquery": "bigquery", + "adbc-duckdb": "duckdb", + "adbc-sqlite": "sqlite", + "adbc-postgres": "postgres", + "adbc-pgvector": "postgres", + "adbc-gizmosql-duckdb": "gizmosql", + } + observed_routes = {case_id: _case_family(_StubCaseId(id=case_id)) for case_id in expected_routes} + assert observed_routes == expected_routes + + +def test_collection_preserves_container_distinct_xdist_groups( + request: pytest.FixtureRequest, pytestconfig: pytest.Config +) -> None: + """Container-distinct xdist_group markers survive multi-level collector normalization without compound leakage.""" + adapters_root = Path(__file__).resolve().parent + group_family_map = { + "alloydb": "postgres", + "gizmosql_sqlite": "gizmosql", + "mariadb": "mysql", + "oracle_18c": "oracle", + "oracle_adb": "oracle", + "paradedb": "postgres", + "pgvector": "postgres", + "snowflake": "snowflake", + } + items: list[pytest.Item] = [] + preserved_expectations: list[tuple[pytest.Item, str]] = [] + for group_name in sorted(_PRESERVED_CONTAINER_XDIST_GROUPS): + family = group_family_map.get(group_name, "postgres") + use_keyword = group_name in {"gizmosql_sqlite", "mariadb", "oracle_adb", "paradedb"} + family_pkg = _StubAdapterCollector.from_parent( + request.session, name=family, path=adapters_root / family / "__init__.py" + ) + family_pkg.add_marker(pytest.mark.xdist_group("cockroachdb" if family == "cockroach" else family)) + module_collector = _StubAdapterCollector.from_parent( + family_pkg, name=f"test_{group_name}.py", path=adapters_root / family / f"test_{group_name}.py" + ) + group_marker = pytest.mark.xdist_group(name=group_name) if use_keyword else pytest.mark.xdist_group(group_name) + module_collector.add_marker(group_marker) + first_item = _StubAdapterItem.from_parent( + module_collector, name=f"test_{group_name}_first", path=module_collector.path + ) + second_item = _StubAdapterItem.from_parent( + module_collector, name=f"test_{group_name}_second", path=module_collector.path + ) + items.extend((first_item, second_item)) + preserved_expectations.extend(((first_item, group_name), (second_item, group_name))) + + oracle_pkg = _StubAdapterCollector.from_parent( + request.session, name="oracledb", path=adapters_root / "oracle" / "oracledb" / "__init__.py" + ) + oracle_pkg.add_marker(pytest.mark.xdist_group("oracle")) + oracle_json_mod = _StubAdapterCollector.from_parent( + oracle_pkg, + name="test_json_storage_modes.py", + path=adapters_root / "oracle" / "oracledb" / "test_json_storage_modes.py", + ) + oracle_json_mod.add_marker(pytest.mark.xdist_group("oracle")) + default_oracle_item = _StubAdapterItem.from_parent( + oracle_json_mod, name="test_native_json_round_trip_matrix", path=oracle_json_mod.path + ) + oracle_18c_item = _StubAdapterItem.from_parent( + oracle_json_mod, name="test_blob_is_json_round_trip_on_oracle_18c", path=oracle_json_mod.path + ) + oracle_18c_item.add_marker(pytest.mark.xdist_group("oracle_18c")) + oracle_adb_cls = _StubAdapterCollector.from_parent( + oracle_json_mod, name="TestOracleAdbWallet", path=oracle_json_mod.path + ) + oracle_adb_cls.add_marker(pytest.mark.xdist_group(name="oracle_adb")) + oracle_adb_class_item = _StubAdapterItem.from_parent( + oracle_adb_cls, name="test_wallet_mtls", path=oracle_json_mod.path + ) + + cloud_mod = _StubAdapterCollector.from_parent( + request.session, + name="test_cloud_connectors.py", + path=adapters_root / "postgres" / "asyncpg" / "test_cloud_connectors.py", + ) + cloud_mod.add_marker(pytest.mark.xdist_group("google_cloud")) + cloud_postgres_item = _StubAdapterItem.from_parent(cloud_mod, name="test_cloud_sql_connector", path=cloud_mod.path) + + ungrouped_postgres = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_postgres", path=adapters_root / "postgres" / "test_driver.py" + ) + overridden_postgres = _StubAdapterItem.from_parent( + request.session, name="test_overridden_postgres", path=adapters_root / "postgres" / "test_driver.py" + ) + overridden_postgres.add_marker(pytest.mark.xdist_group(name="custom_transient_group")) + ungrouped_cockroach = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_cockroach", path=adapters_root / "cockroach" / "test_retry.py" + ) + ungrouped_snowflake = _StubAdapterItem.from_parent( + request.session, name="test_ungrouped_snowflake", path=adapters_root / "snowflake" / "test_adbc.py" + ) + items.extend(( + default_oracle_item, + oracle_18c_item, + oracle_adb_class_item, + cloud_postgres_item, + ungrouped_postgres, + overridden_postgres, + ungrouped_cockroach, + ungrouped_snowflake, + )) + + pytest_collection_modifyitems(pytestconfig, items) + + for item, expected_group in preserved_expectations: + assert _xdist_groups(item) == [expected_group] + assert _xdist_groups(default_oracle_item) == ["oracle"] + assert _xdist_groups(oracle_18c_item) == ["oracle_18c"] + assert _xdist_groups(oracle_adb_class_item) == ["oracle_adb"] + assert _xdist_groups(cloud_postgres_item) == ["postgres"] + assert _xdist_groups(ungrouped_postgres) == ["postgres"] + assert _xdist_groups(overridden_postgres) == ["postgres"] + assert _xdist_groups(ungrouped_cockroach) == ["cockroachdb"] + assert _xdist_groups(ungrouped_snowflake) == ["snowflake"] + + +class _StubCaseId(msgspec.Struct, frozen=True, gc=False): + id: str + + +class _StubAdapterCollector(pytest.Collector): + def collect(self) -> list[pytest.Item | pytest.Collector]: + return [] + + +class _StubAdapterItem(pytest.Item): + def runtest(self) -> None: + return None + + +def _xdist_groups(item: pytest.Item) -> list[str]: + groups: list[str] = [] + for mark in item.iter_markers(name="xdist_group"): + if mark.args: + groups.append(str(mark.args[0])) + elif (name := mark.kwargs.get("name")) is not None: + groups.append(str(name)) + return groups diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 45d667e69..7b67de289 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,18 +1,18 @@ """Pytest configuration and fixtures for integration tests.""" import pkgutil -from importlib.util import find_spec from typing import TYPE_CHECKING import pytest import tests.integration.fixtures as fixtures_pkg +from tests.fixtures.modules import module_is_available from tests.fixtures.rustfs import ensure_rustfs_bucket if TYPE_CHECKING: from pytest_databases.docker.rustfs import RustfsService -pytest_plugins = [ +_DATABASE_PLUGINS: tuple[str, ...] = ( "pytest_databases.docker.azure_blob", "pytest_databases.docker.bigquery", "pytest_databases.docker.cockroachdb", @@ -27,7 +27,7 @@ "pytest_databases.docker.rustfs", "pytest_databases.docker.snowflake", "pytest_databases.docker.spanner", -] +) _NON_PLUGIN_FIXTURE_MODULES: frozenset[str] = frozenset({"cleanup"}) @@ -54,6 +54,9 @@ def pytest_configure(config: pytest.Config) -> None: """Import integration fixture plugins whose backing driver packages are installed.""" + for plugin in _DATABASE_PLUGINS: + if not config.pluginmanager.has_plugin(plugin): + config.pluginmanager.import_plugin(plugin) ordered_modules = sorted( pkgutil.iter_modules(fixtures_pkg.__path__), key=lambda item: (item.name == "adapter_cases", item.name) ) @@ -74,14 +77,6 @@ def rustfs_bucket_name(rustfs_service: "RustfsService", rustfs_default_bucket_na return ensure_rustfs_bucket(rustfs_service, rustfs_default_bucket_name) -def _package_available(package: str) -> bool: - """Return True when a top-level or dotted package specification can be resolved.""" - try: - return find_spec(package) is not None - except ModuleNotFoundError: - return False - - def _requirements_satisfied(requirements: tuple[tuple[str, ...], ...]) -> bool: """Return True when at least one required driver package group is importable.""" - return any(all(_package_available(package) for package in group) for group in requirements) + return any(all(module_is_available(package) for package in group) for group in requirements) diff --git a/tests/integration/fixtures/mysql.py b/tests/integration/fixtures/mysql.py index 6a6f3b052..39bcb663f 100644 --- a/tests/integration/fixtures/mysql.py +++ b/tests/integration/fixtures/mysql.py @@ -63,7 +63,7 @@ __all__ = ( "MariadbDriverSession", - "MysqlAdkResidualContext", + "MysqlAdkStoreHarness", "aiomysql_clean_driver", "aiomysql_config", "aiomysql_driver", @@ -71,7 +71,7 @@ "asyncmy_config", "asyncmy_driver", "mariadb_driver_session", - "mysql_adk_residual_context", + "mysql_adk_store_harness", "mysqlconnector_async_config", "mysqlconnector_async_driver", "mysqlconnector_clean_async_driver", @@ -94,8 +94,8 @@ } -class MysqlAdkResidualContext(msgspec.Struct, frozen=True, gc=False): - """Unified test harness for MySQL ADK residual schema, precision, and FK checks.""" +class MysqlAdkStoreHarness(msgspec.Struct, frozen=True, gc=False): + """Unified test harness for MySQL ADK store schema, precision, and FK checks.""" driver_mode: str is_async: bool @@ -410,9 +410,9 @@ def pymysql_clean_driver(pymysql_config: "PyMysqlConfig") -> "Generator[PyMysqlD @pytest.fixture(params=("aiomysql", "asyncmy", "mysqlconnector-async", "mysqlconnector-sync", "pymysql")) -async def mysql_adk_residual_context( +async def mysql_adk_store_harness( request: pytest.FixtureRequest, mysql_service: "MySQLService" -) -> "AsyncGenerator[MysqlAdkResidualContext, None]": +) -> "AsyncGenerator[MysqlAdkStoreHarness, None]": """Provide isolated ADK stores across all MySQL driver modes with deterministic pool teardown.""" pytest.importorskip("google.adk") driver_mode = str(request.param) @@ -488,7 +488,7 @@ async def mysql_adk_residual_context( store.create_tables() fk_store.create_tables() - yield MysqlAdkResidualContext( + yield MysqlAdkStoreHarness( driver_mode=driver_mode, is_async=is_async, store=store, diff --git a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py index ff02b1aee..7e8bcc50f 100644 --- a/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py +++ b/tests/unit/adapters/test_adbc/test_adbc_config_normalization.py @@ -711,7 +711,8 @@ def test_db2_cli_dsn_normalizes_to_db2_uri_and_credentials() -> None: odbc_cfg = AdbcConfig( connection_config={"dsn": "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=testdb;HOSTNAME=localhost;PORT=50000;"} ) - assert _resolve_driver_name(odbc_cfg) == "adbc_driver_sqlite.dbapi.connect" + assert _resolve_driver_name(odbc_cfg) != "adbc_driver_db2.dbapi.connect" + assert odbc_cfg.statement_config.dialect != "db2" def test_adbc_driver_db2_routes_parameterized_non_query_via_executemany() -> None: diff --git a/tests/unit/adapters/test_oracledb/test_core_driver_features.py b/tests/unit/adapters/test_oracledb/test_core_driver_features.py index 234e088ad..58cee2653 100644 --- a/tests/unit/adapters/test_oracledb/test_core_driver_features.py +++ b/tests/unit/adapters/test_oracledb/test_core_driver_features.py @@ -5,10 +5,11 @@ import io import os import shutil +import ssl import zipfile from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import pytest from cryptography import x509 @@ -25,6 +26,7 @@ OracleEndpoint, OracleTopologyConfig, apply_driver_features, + apply_wallet_ssl_context, build_duality_etag_update_sql, build_tns_description, extract_duality_etag, @@ -477,3 +479,30 @@ def _build_test_p12_bytes(password: bytes | None = b"WalletSecret123") -> bytes: return pkcs12.serialize_key_and_certificates( name=b"oracle-wallet", key=private_key, cert=cert, cas=[cert], encryption_algorithm=encryption ) + + +def test_apply_wallet_ssl_context_with_config_dir_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """apply_wallet_ssl_context resolves ewallet.pem via config_dir when wallet_location is missing.""" + p12_bytes = _build_test_p12_bytes(None) + wallet_dir = tmp_path / "wallet_via_config_dir" + wallet_dir.mkdir() + (wallet_dir / "ewallet.p12").write_bytes(p12_bytes) + extract_oracle_wallet_zip(wallet_dir, target_dir=wallet_dir) + assert (wallet_dir / "ewallet.pem").is_file() + + fake_ctx = ssl.create_default_context() + fake_ctx.verify_flags |= ssl.VERIFY_X509_STRICT + monkeypatch.setattr(ssl, "create_default_context", lambda: fake_ctx) + + conn_config: dict[str, Any] = {"config_dir": str(wallet_dir)} + apply_wallet_ssl_context(conn_config) + assert "ssl_context" in conn_config + assert conn_config["ssl_context"] is not None + assert not (conn_config["ssl_context"].verify_flags & ssl.VERIFY_X509_STRICT) + + +def test_apply_wallet_ssl_context_noop_when_no_wallet() -> None: + """apply_wallet_ssl_context does nothing when neither wallet_location nor config_dir is set.""" + conn_config: dict[str, Any] = {"user": "app", "password": "secret"} + apply_wallet_ssl_context(conn_config) + assert "ssl_context" not in conn_config diff --git a/tests/unit/test_import_time.py b/tests/unit/test_import_time.py index cf01e38a1..2494226d1 100644 --- a/tests/unit/test_import_time.py +++ b/tests/unit/test_import_time.py @@ -8,8 +8,6 @@ import pytest -from tests.conftest import should_run_optional_cloud_suite - FORBIDDEN = ( "pandas", "polars", @@ -191,20 +189,9 @@ def test_no_unintended_cross_adapter_imports() -> None: assert not violations, "Found cross-adapter imports:\n" + "\n".join(violations) -def test_cloud_emulator_opt_in_accepts_cli_flag_or_env_var(monkeypatch: pytest.MonkeyPatch) -> None: - """Cloud emulator opt-in runs when CI is active or either CLI flag or env var is enabled.""" - monkeypatch.delenv("CI", raising=False) - monkeypatch.delenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", raising=False) - - assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is False - assert should_run_optional_cloud_suite(True, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True - - monkeypatch.setenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", "1") - assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True - - monkeypatch.setenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", "true") - assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True - - monkeypatch.delenv("SQLSPEC_ENABLE_BIGQUERY_TESTS", raising=False) - monkeypatch.setenv("CI", "true") - assert should_run_optional_cloud_suite(False, "SQLSPEC_ENABLE_BIGQUERY_TESTS") is True +def test_pytest_collect_only_succeeds_without_plugin_registration_errors() -> None: + """Verifies that root pytest collection succeeds without non-top-level plugin errors.""" + result = subprocess.run( + [sys.executable, "-m", "pytest", "--collect-only", "-q", "tests"], capture_output=True, text=True, check=False + ) + assert result.returncode == 0, f"pytest --collect-only failed:\n{result.stderr}\n{result.stdout}" diff --git a/tests/unit/utils/test_fixtures.py b/tests/unit/utils/test_fixtures.py index 42a360c48..e7b82f569 100644 --- a/tests/unit/utils/test_fixtures.py +++ b/tests/unit/utils/test_fixtures.py @@ -1825,7 +1825,11 @@ def test_sequence_resync_statements_cross_dialect() -> None: ) assert len(stmts_duckdb) == 1 assert "count(nextval(" in stmts_duckdb[0][0] - assert stmts_duckdb[0][1] == {"sequence_name": "users_id_seq"} + assert stmts_duckdb[0][1] == { + "sequence_schema": "main", + "sequence_name": "users_id_seq", + "quoted_seq": "main.users_id_seq", + } stmts_tsql = fixture_module._sequence_resync_statements("users", [col_ident], dialect_name="tsql", family="tsql") assert len(stmts_tsql) == 1 @@ -2716,3 +2720,31 @@ def test_duckdb_sequence_resync_round_trip(tmp_path: Path) -> None: assert next_id == 26 finally: config.close_pool() + + +def test_duckdb_sequence_resync_increment_and_schema(tmp_path: Path) -> None: + """DuckDB sequence resync handles non-unit increment, schema qualification, and empty tables.""" + (tmp_path / "custom.items.json").write_text(json.dumps([{"id": 11, "name": "first"}]), encoding="utf-8") + (tmp_path / "custom.empty_items.json").write_text(json.dumps([]), encoding="utf-8") + config = DuckDBConfig(connection_config={"database": ":memory:"}) + try: + with config.provide_session() as driver: + driver.execute("CREATE SCHEMA custom") + driver.execute("CREATE SEQUENCE custom.items_id_seq INCREMENT BY 2 START 1") + driver.execute("CREATE SEQUENCE custom.empty_id_seq START 1") + driver.execute( + "CREATE TABLE custom.items (id INTEGER DEFAULT nextval('custom.items_id_seq') PRIMARY KEY, name VARCHAR)" + ) + driver.execute( + "CREATE TABLE custom.empty_items (id INTEGER DEFAULT nextval('custom.empty_id_seq') PRIMARY KEY, name VARCHAR)" + ) + counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) + assert counts == {"custom.items": 1, "custom.empty_items": 0} + driver.execute("INSERT INTO custom.items (name) VALUES ('next-after-fixture')") + next_id = driver.select_value("SELECT id FROM custom.items WHERE name = 'next-after-fixture'") + assert next_id == 13 + driver.execute("INSERT INTO custom.empty_items (name) VALUES ('first-after-empty')") + empty_next_id = driver.select_value("SELECT id FROM custom.empty_items WHERE name = 'first-after-empty'") + assert empty_next_id == 1 + finally: + config.close_pool() diff --git a/uv.lock b/uv.lock index 014048af6..1098d063a 100644 --- a/uv.lock +++ b/uv.lock @@ -19,30 +19,6 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. -exclude-newer-span = "P7D" - -[options.exclude-newer-package] -advanced-alchemy = false -litestar = false -litestar-asyncapi = false -litestar-asyncpg = false -litestar-autowire = false -litestar-email = false -litestar-granian = false -litestar-htmx = false -litestar-mcp = false -litestar-oracledb = false -litestar-queues = false -litestar-saq = false -litestar-security = false -litestar-vite = false -polyfactory = false -pytest-databases = false -sqlglot = false -sqlglotc = false - [manifest] overrides = [ { name = "mysql-connector-python", marker = "python_full_version < '3.12'" }, @@ -69,7 +45,7 @@ wheels = [ [[package]] name = "adbc-driver-db2" -version = "0.2.1" +version = "0.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, @@ -77,27 +53,27 @@ dependencies = [ { name = "pyarrow" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/4b/48c9619cc81c8cbef39cb9c3253bd91005c84067078d7508ee1047daaf7d/adbc_driver_db2-0.2.1-py3-none-macosx_12_0_universal2.whl", hash = "sha256:3573a21ef6f22ab8b66062d982bbafef99a16ca19fb7069e85dfa3c7a79b4a87", size = 3979102, upload-time = "2026-09-30T16:12:32.278Z" }, - { url = "https://files.pythonhosted.org/packages/da/ff/673cede5fe0a4942526139829e29f1338bdcbb75381989e48c03e7296bda/adbc_driver_db2-0.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:286da0af1d0c64676986792f6331592e9e8c2bde5a151877c46e7961c54204de", size = 7507117, upload-time = "2026-09-30T16:12:34.021Z" }, - { url = "https://files.pythonhosted.org/packages/b3/af/6b70086e8873d5d64e961bdcf842d9b64bed2046580b59d56826d789909c/adbc_driver_db2-0.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ccd8eb2ffe39a439ca36e186c38d515defe64227c8e5a295f2ee87f5a3f26b37", size = 8280573, upload-time = "2026-09-30T16:12:36.045Z" }, - { url = "https://files.pythonhosted.org/packages/60/80/6679772b54d968226452cd700b18fa7d09d54fe0e0d254c5615396be9ce4/adbc_driver_db2-0.2.1-py3-none-win_amd64.whl", hash = "sha256:9eed1d3de68d868cef7df835cfd1f5930a882b2455222b6b178e13ebbbdd8aab", size = 8134110, upload-time = "2026-09-30T16:12:38.749Z" }, + { url = "https://files.pythonhosted.org/packages/95/85/cdd32e94bcb2a84e332860ec53cb28de53f19ec1a21eebc6955cfb8fca7c/adbc_driver_db2-0.2.3-py3-none-macosx_12_0_universal2.whl", hash = "sha256:865705599ddd5bb8bdedfbc6fa140767578d801770a057dd4d4c2da5f44718b1", size = 4198549, upload-time = "2026-10-27T13:52:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/ad54c5b2c987969f8817b4324214608df6e17e52b8b42215937015a785a5/adbc_driver_db2-0.2.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6466a9477a821db9c9e8a0cb1651cb8176c69aa6af68f35d51278d226cbfb12f", size = 7961304, upload-time = "2026-10-27T13:52:54.501Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6e/d9ad4c99b4a00371ccf42fc69d8bcce675628b49f636fe7ba45eff7d1661/adbc_driver_db2-0.2.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9477387261211d64ece7531fc15965243220e3119ed3a0faaa506d43db46d000", size = 8862031, upload-time = "2026-10-27T13:52:56.314Z" }, + { url = "https://files.pythonhosted.org/packages/82/ba/0ad08234f3cf70264d659fff5638e5609095f94a9d06197f980cf62d1b90/adbc_driver_db2-0.2.3-py3-none-win_amd64.whl", hash = "sha256:0d601bea536d6fa2d084a3bd94a9414d9721b884d858f4da6246d5575b0e5336", size = 8822996, upload-time = "2026-10-27T13:52:58.345Z" }, ] [[package]] name = "adbc-driver-flightsql" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, - { name = "importlib-resources" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/91/605b35b97aa5972a8026c0687e9996658d1d10ff7a0624173a0d0c7c5b93/adbc_driver_flightsql-1.12.0.tar.gz", hash = "sha256:300f67801ea016578e0c4bb798fc2b9024e405cfdd3f79fd77e37481a5767e45", size = 22097, upload-time = "2026-07-28T00:43:02.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/9c/a2dfca043e473cb95d4db32d6f37ee4c1c65bf22c84fb77526c4b698a47a/adbc_driver_flightsql-1.13.0.tar.gz", hash = "sha256:b067abf364e5253c033ce86dd1f8319eb4780d266eddc9b366cb1a1149daf9c1", size = 21808, upload-time = "2026-10-26T06:33:34.782Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/a4/5ad52abf0c4830585b86d68cff8ce07779cdee9b8cf390dff0ee21a29fa7/adbc_driver_flightsql-1.12.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:c2c7209407c180d9d0dd94846203f184ab91e116782585bf29cb968c2686ba75", size = 8146345, upload-time = "2026-07-28T00:41:22.824Z" }, - { url = "https://files.pythonhosted.org/packages/61/36/49267a4fc8c07c066dd8c36b9c3d27265a4dc0fc478cb5acb7bd6a551d80/adbc_driver_flightsql-1.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fb37efefceb7ceca64840c76c55e6280f3811b54dca305ec1781f9cc1a6da21e", size = 7552045, upload-time = "2026-07-28T00:41:27.147Z" }, - { url = "https://files.pythonhosted.org/packages/2b/5b/f8566aa6d05eb40225f5874f8f5f5e8b8b4a4ce866d817a5ff352f326302/adbc_driver_flightsql-1.12.0-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69742e4f8fcc254490aa7f8e254a3b704c24bde9d5d827a0926ea90a6b4c3a6d", size = 14981635, upload-time = "2026-07-28T00:41:33.2Z" }, - { url = "https://files.pythonhosted.org/packages/57/45/6468fb425f3c0ae868c1ca9ab8e15cba76b59e941edd7e80e892e5476728/adbc_driver_flightsql-1.12.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9adfb5e46abc77347476d09ee76dfc835fef2b4dcb0c76d08d6d35aa3a4cce17", size = 13699634, upload-time = "2026-07-28T00:41:38.232Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/6773dedc54fad73277ee2d767dd817e203e949972b0cfb197df99e00b784/adbc_driver_flightsql-1.12.0-py3-none-win_amd64.whl", hash = "sha256:098f2498778fd3efd671fbeacba80987de98ebfeb0291da77fe0a26c6fa78532", size = 15033803, upload-time = "2026-07-28T00:41:42.483Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/8de9331757d5bb8c5bdaf8cb3221d4adf34c2f56d593dcaeb399c012d972/adbc_driver_flightsql-1.13.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:e73d8e456805b8fae8274e84772864d06481b41a9969c5ea2aed74893f31fc8d", size = 8326058, upload-time = "2026-10-26T06:31:19.469Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3d/217cec4923d82310dde0dfc3b9cb853a1178503b5ac690190ac8450f5236/adbc_driver_flightsql-1.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0a6e7edee439e35490d2ec49dea7463635a9985e0dba6b61fd78f42cda776fa1", size = 7715852, upload-time = "2026-10-26T06:31:25.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/af/5d0ec5ebf52478728e775be4e36ccf3c605cce719b2478facad4e92663d0/adbc_driver_flightsql-1.13.0-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1199552b5cfe3349554a972c372fcaa2ecfdfac27b942c8ddf8c3bf798690c31", size = 15275335, upload-time = "2026-10-26T06:31:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/89d9c4fa900bd7f8d1ab0443e24fad1e301820f4219ed7eb5277fa202181/adbc_driver_flightsql-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38886f49e3504c0b226796bcae914cf32293b79104c273459510c36592880749", size = 13974565, upload-time = "2026-10-26T06:31:37.661Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d6/5e5606bd95e6c87ba4c86e0dfcf41e1ad2300abd7b9e7cf2501e51d4ad1c/adbc_driver_flightsql-1.13.0-py3-none-win_amd64.whl", hash = "sha256:6f11164214d979abb61600d96479781c7c47ea79f4f1c843837faeb772724f25", size = 15019143, upload-time = "2026-10-26T06:31:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a0/3e792af07fe018f0cf640b7b036e54eb5133d81a0c08b554d6feb3c57734/adbc_driver_flightsql-1.13.0-py3-none-win_arm64.whl", hash = "sha256:ea5e26a4a9df4da69c9a39cd90a5475eaf084a5f2b52fc3e8fe68b1d54a290dc", size = 13686797, upload-time = "2026-10-26T06:31:48.625Z" }, ] [[package]] @@ -119,43 +95,60 @@ wheels = [ [[package]] name = "adbc-driver-manager" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/f8/ed6475b49a7cf35ea888d5c95e7d4bc9dc6568f9d741f14c0573d622cc1e/adbc_driver_manager-1.12.0.tar.gz", hash = "sha256:45991f0c2de369d330c6a211ca2edbcce6389c5dc81cde70461bdeb6f8f7b268", size = 217579, upload-time = "2026-07-28T00:43:03.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/2d/e41ea911f9486c497534ae181dfdab19adca21f71abc8a1fcaf2c27251a8/adbc_driver_manager-1.12.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:3c0c73670c8aa6fe42de1d5e71a0b329c4b37f7c55c560c23f6f3a1609200c1f", size = 600030, upload-time = "2026-07-28T00:41:54.753Z" }, - { url = "https://files.pythonhosted.org/packages/10/ea/1a8b51999785d7dce17079dd635c9ee2372c75ec7328423ce862741cb503/adbc_driver_manager-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6943c7adcf3c7c9f7c4b5bdb7589c331027a347e3c77471eb3f656b1a881e351", size = 611058, upload-time = "2026-07-28T00:41:56.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/a2/5ede53173a420742fa71d6c26792e2295fc73f25cf39055f912a5385b1f0/adbc_driver_manager-1.12.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c9936adb280e2c10e90632e41b58aa23be358e1136d8fb3c52862b72818a95", size = 4663735, upload-time = "2026-07-28T00:41:58.793Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1a/781561d0f55e05a0b884244ed563dab14b165b4dd74abd8af3f8efd95e3d/adbc_driver_manager-1.12.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30d96ab4a2594b4109496fb4913646f41a5bf1ecce79b4313847d240a2a62db3", size = 4747090, upload-time = "2026-07-28T00:42:00.746Z" }, - { url = "https://files.pythonhosted.org/packages/c6/fa/47c755a74ea4887968c52a968e02736007da4042fc0820292dc8c6827a94/adbc_driver_manager-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:67419b92c286646944426992069f56fed90c2ceac83521f6d66d7d3cbf6c17ea", size = 760952, upload-time = "2026-07-28T00:42:02.432Z" }, - { url = "https://files.pythonhosted.org/packages/de/8c/cd3fe16df716719116a6c79e64a768fe994f6ded55d5a8f091bb4f42d6f0/adbc_driver_manager-1.12.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fd02364c65b8b376c5627e3b77410f457fcbbf983e52e8d15ca099da3a7ae314", size = 599054, upload-time = "2026-07-28T00:42:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/49/4a/2f060ff6bd61420ea1613670e1f85a22a8714934c235186dc3803de8ddac/adbc_driver_manager-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d8dcf62621090e8d9c8216e08dfc4043f16331872522186af61a5de9478e9c63", size = 609964, upload-time = "2026-07-28T00:42:05.82Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/0746db149828ae91e4a6cf49f8d0e49210eec20c03ad80044454139c8240/adbc_driver_manager-1.12.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa5dbbf101962d212b176f25e6fc509dacf07afd4cf70b5027d81ec6871bdec", size = 4685726, upload-time = "2026-07-28T00:42:08.1Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c3/f8e9c5157b19e986df719259eb3502dad1268df9f7a1034f65ca220ab2ea/adbc_driver_manager-1.12.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b340679a005a8adf6b0b58754dbc638dff00db7b2559c140406a1d92678b48c", size = 4768774, upload-time = "2026-07-28T00:42:10.359Z" }, - { url = "https://files.pythonhosted.org/packages/92/51/f8e625af691e6b4c54945790854524356a02a0a69063e888f7cfee1b2e50/adbc_driver_manager-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:47f428a922d224fd486b661deeaf9520e5faec558b3d144832bed09a080cac88", size = 760087, upload-time = "2026-07-28T00:42:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f9/674c5bbc5093617d72c4f58a5dab67982710b2320cc9aa826050a6aaa131/adbc_driver_manager-1.12.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:c42ca4d9caa22b3a5ce76bde8729169f403bb7393e3671734b9416634c207125", size = 596815, upload-time = "2026-07-28T00:42:13.64Z" }, - { url = "https://files.pythonhosted.org/packages/56/5f/c1d888d787330801edae282d2a9def3765e8157547cc20e71154ff38c1bb/adbc_driver_manager-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c894117c8f5c484b902c8b070bcfd9d31d90efe0288b2b58a3ddab97c80f66e7", size = 608277, upload-time = "2026-07-28T00:42:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/06/4b/ee799babf171e39690ef45560451096f869d9e7387bc0e5a754bb243ed2a/adbc_driver_manager-1.12.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:214f80f9b65562f08b4d1c52a756b5db557530e3c0652f587c43aaa80039579a", size = 4667230, upload-time = "2026-07-28T00:42:17.97Z" }, - { url = "https://files.pythonhosted.org/packages/00/c6/a35e38ef5e0db391be79e0e14c019ce378b87d9d7e31d1dfcd451e9d291f/adbc_driver_manager-1.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532ab290b3d923ce0a75bca21dc6e13f55835625f78808e1664755939f3ebdf6", size = 4745299, upload-time = "2026-07-28T00:42:20.189Z" }, - { url = "https://files.pythonhosted.org/packages/16/e2/62bacd6844859036d79ea229401b5200056fb5050c82dc3a2e28b08ff49b/adbc_driver_manager-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:034da82c1a6e195d67ca1f0c97a1a517046037ec3029ab9a0ea8f7ccb14056e4", size = 758878, upload-time = "2026-07-28T00:42:21.598Z" }, - { url = "https://files.pythonhosted.org/packages/50/ea/f53b434fe36d0f138d147fc10a95784c8c0eeea1bec1f3f31eee5ec8bdb5/adbc_driver_manager-1.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a740d634118722f42af31176374fddbad3846fa2e6536f497bac145e9511cecc", size = 597579, upload-time = "2026-07-28T00:42:23.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/57/6208e66d9256550c2aff75db4a323a855a0d5d2d1bd639526f825d3e08b4/adbc_driver_manager-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8a77ae39832e67946009816d83c321e540a3024aad1419ccba24ddeb7b6a01f4", size = 610337, upload-time = "2026-07-28T00:42:25.051Z" }, - { url = "https://files.pythonhosted.org/packages/1d/cd/f5ea3f08191af5ae15041821fcb52bf35837dce1a9ac16fa039b3bfe308c/adbc_driver_manager-1.12.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:690f140ca67d49f995afac59f85441c3d5e896cd2fc8fd381423fe900e51f1f7", size = 4664297, upload-time = "2026-07-28T00:42:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/81/823a71a515078545eab8a4be8381206887129e11b91e9bf51ca2a9eea44d/adbc_driver_manager-1.12.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd568c94874c0586d82f99de2bb5d2c02b4fa9c5bafe3d0d8ab353bddf9d2fd6", size = 4733739, upload-time = "2026-07-28T00:42:29.814Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f7/7612d078d935344aee679a44a6283de6aae9008eb8e0ef80e475dd12dffa/adbc_driver_manager-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:57f5101fb2a853b1ffb81ff807b5e29a51ba14c64032eb0038b8dfd433b6d533", size = 777952, upload-time = "2026-07-28T00:42:40.881Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ad/2478338aaece38b8b72259dbfd4d4c84d9a038421e25bbc283e510d47555/adbc_driver_manager-1.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bb9db6e4a3bcd73153435a900b5ae40ad36f5875df93a8faf784d9fcf6833983", size = 615694, upload-time = "2026-07-28T00:42:31.932Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/0592c85e653f005aa28de7733b3c3c4f0282238301694f76806e5f3cc1e1/adbc_driver_manager-1.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:07cae26bd5ccee6caa4227f817c0fd57f9ac131c2dd98e0c5d7fecfef61819c7", size = 628341, upload-time = "2026-07-28T00:42:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/65705a72f768bc2dda82623a74cf816609dfdff56f3ad22b073d4a1ea7f8/adbc_driver_manager-1.12.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442ed2ee8ea62c475bf3478385555bb4f0b25d9d551087ffe40c73b91bf5431e", size = 4730268, upload-time = "2026-07-28T00:42:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/44/b9/60ecde5d9dde5acc5576cb0ba5ffa34e154464e07fa295c57cd975ea27c7/adbc_driver_manager-1.12.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c2aa05c5dc52164692284b2df27fba5680dbc967b8e3ca704aabf5399667996", size = 4777527, upload-time = "2026-07-28T00:42:37.709Z" }, - { url = "https://files.pythonhosted.org/packages/ac/76/6749e0c0c437219780c65487cff67dc09a556c1fccf577a2b27f7b92a704/adbc_driver_manager-1.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:cfa08f8c7c63e3fa92eb4e26ef4d8a9520cf92a39281cd011821f6f16a963080", size = 793451, upload-time = "2026-07-28T00:42:39.222Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/db/73/114b15fff653d53a5588826152e33b009c141edcd45678506f9cbdf06c19/adbc_driver_manager-1.13.0.tar.gz", hash = "sha256:72d616e43d03b735e4000b653307531a2d1820f1f7e0ce949656f0ab939bd53e", size = 220657, upload-time = "2026-10-26T06:33:35.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/ec/48d773b8af10a028010bd66ee72c7f5bbe4773771e388bff63bd9c223493/adbc_driver_manager-1.13.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:7e76996b5adb33f918d2fb8480f2f8da333fe2f6d99c2ade5b2d6deb63ecf1eb", size = 584036, upload-time = "2026-10-26T06:31:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/d0/54/89e374dab257558582beb53ef6569f30d761f5aafaf59c34ad5562c9bd22/adbc_driver_manager-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d0c0dbd225cd670722929fd72178498d4104f16b95db390a372a7bea9ada39a0", size = 597785, upload-time = "2026-10-26T06:31:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7a/2dcd5cd355c917b1436e94f4bfd6f6aab960993dfeda36cd4723d8c76236/adbc_driver_manager-1.13.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2da4174cec7d52269d144a847e60e6c80d7b28317764d204b436be4ed7fd294e", size = 4150783, upload-time = "2026-10-26T06:31:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2d/97675f53a3dec3ed0745d087f7eb26a4601a2879d9918f8ab24bd9c49b2a/adbc_driver_manager-1.13.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e5444a944e3c17c778601e35e83902e26d9571e77ca132b61f264c78346a06c", size = 4219333, upload-time = "2026-10-26T06:31:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/9026fa768c47860a38fd81446eab768cc093fc49ff8f202c9f2a2a2a3a0f/adbc_driver_manager-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:26c6c125ba569a4f269d4d2c9e462b9de08b7c3d7fec931fe17e0b750ace3b6d", size = 749255, upload-time = "2026-10-26T06:31:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ef/cc43bf835198307daed40cb38f32c12c120617a17ab800b16334d56b8c67/adbc_driver_manager-1.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:7f019e7cf4fdd85d890d94934de3912416b23028fcd256cac54d34a81ad2f2a6", size = 902200, upload-time = "2026-10-26T06:32:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/b1/da/32a87b4c3bf7374789ee9cd4e30af36f6197dd909a1d41d8c37b0abb3130/adbc_driver_manager-1.13.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7a4caf0a2edcee471f3bf2e92f31c8df62a6facf6ceee51d9d6d3ea9b6f10aec", size = 586271, upload-time = "2026-10-26T06:32:02.111Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b9/16a51949215a7d1d2d0212796758ba65cc3c02870c9b9a5f8a2f417e2dfa/adbc_driver_manager-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa07d0453c0a1022f4ab5490dc1795372799f994424cb8fd6fb274eda8fffb2e", size = 598566, upload-time = "2026-10-26T06:32:03.453Z" }, + { url = "https://files.pythonhosted.org/packages/31/ac/02fbdb2552f23794b62dfb6d9ecb5391767f018d28c180c917226b118c4e/adbc_driver_manager-1.13.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46776b55bddb98d37908bc4809a4567f6f897f97d1a79bf4ab0889bcab4ef585", size = 4148672, upload-time = "2026-10-26T06:32:05.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0d/ae1ca761707904fe502cb63204c40d6baae7589c4181123cdda3aa03663f/adbc_driver_manager-1.13.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47e225d206d0d5f82d32d1ad0985ddf4984168479ecfcff21cb82fc5bb36ec5a", size = 4217627, upload-time = "2026-10-26T06:32:08.134Z" }, + { url = "https://files.pythonhosted.org/packages/5e/56/7f2cb5ff0554f33bc2d4ef5871f739dc584fb3d1361f22cd8cb4228718b1/adbc_driver_manager-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf0ebd3d6082399f904863c88c5b49f5f8e6026f057bac2ebb1a9a2206325569", size = 746315, upload-time = "2026-10-26T06:32:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/ec/75/c0c3e23487817552fffe63ac62c3bb36dfd258e77c53ece97fd675f2e2c0/adbc_driver_manager-1.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:19bf47dbeaa654c694831d7ef538a027fef7785d62d8860eebd5f24eeeae7463", size = 899028, upload-time = "2026-10-26T06:32:10.948Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e5/710220a20b0966e20205fe18f5a45b69d98c6413e74546143c83abc33105/adbc_driver_manager-1.13.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:624bef3b9949e7038ff451ce3c08df20e0738da23c0d1037834c0ee4426ea305", size = 584189, upload-time = "2026-10-26T06:32:12.653Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/0b4ab4c4dad3de34a19c3fed0833a244355da8ee4800df00b271372051f5/adbc_driver_manager-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c1420b860c87679757058bda49b64a56e7f61e968eb6568a853e1d52356d44b3", size = 596254, upload-time = "2026-10-26T06:32:14.194Z" }, + { url = "https://files.pythonhosted.org/packages/97/11/5d732670417f9d8e2b3d201be366f6ed716041d490008a21469c1baf472e/adbc_driver_manager-1.13.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82c90861896653c2cbc39f74f0261c0bb0c2862a2ceee60f29744df6e18da175", size = 4133894, upload-time = "2026-10-26T06:32:16.578Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/e9098132e7892ba95582ab62880730ed3070a6d0218a04b56ef54a5211ae/adbc_driver_manager-1.13.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b81819bd35fb47b59757ab075041e326ec278ddaec33f75b6baadf11be1bc066", size = 4206979, upload-time = "2026-10-26T06:32:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4f/726bf4db51fa2e9ac1407ea3c1468306a8c015746c5e04ec0687ff404127/adbc_driver_manager-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:76f3addf8f4a25937b4d4dd3aa581b436c60c248123a094353d3293ee3405454", size = 744877, upload-time = "2026-10-26T06:32:20.232Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/33522fc3cfcc8072cd00d408b74375a06973420b0b24e49d754cabbbc637/adbc_driver_manager-1.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d7c8d9a07c99235475c68581cacaa03ee04b31d50440930321dde69c9d47a1bb", size = 897940, upload-time = "2026-10-26T06:32:21.792Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d2/12972831ad00ba4c42c6b14bd009e196443ff98e9466c6239d41d68af05a/adbc_driver_manager-1.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:685cc3b2689a7f16f1c79f9423a6ec1694d1663cb0257aec2006546f294379ce", size = 584835, upload-time = "2026-10-26T06:32:23.914Z" }, + { url = "https://files.pythonhosted.org/packages/47/a0/596e90687f39982443bb5353e68d5b483169423d5c25bd6d0463835ed59c/adbc_driver_manager-1.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:18f03c51b73ffcafe16d98fd537a6417df5171043147e59fc781373d2290ffa9", size = 597713, upload-time = "2026-10-26T06:32:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/07/dd/5363a6103751fa0ac644c0db1875b495e3297b9f15fa04fe5ce4cf2da1ed/adbc_driver_manager-1.13.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7699696b6388fb7e0e69b84627a28bdfe988d144d45649102859a0f07aeb0223", size = 4131883, upload-time = "2026-10-26T06:32:28.05Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/1759a417eae4d90c390737eccf1098033779f2a8d6d8952d2f208cd236fd/adbc_driver_manager-1.13.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ce8256a35e68420fab5e19830b6051578828c7774f10500abae7edc61ec0d3a", size = 4190903, upload-time = "2026-10-26T06:32:30.221Z" }, + { url = "https://files.pythonhosted.org/packages/c9/22/96f0c65ff4c9417da9c7056cebc2e9b08768c9b26f0123896c7b2172c251/adbc_driver_manager-1.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:5d1b71b6faa58ab970c67e6992eee0a0e617d1a2f35d1bd1095d6c4c4dea1e3b", size = 762138, upload-time = "2026-10-26T06:32:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0a/1b53bb18f406f2c808b2c3757318f905d61901254c83071d3869648e0c3c/adbc_driver_manager-1.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:051147056936bcec489c59c859162a4bf17cb6280efc5b75f63e70b2ac4250bf", size = 921713, upload-time = "2026-10-26T06:32:45.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/dc/7151b3fedcb5d1ae0301eb5c1389200616a42f3138d1d55f142603a4001b/adbc_driver_manager-1.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:487ae59fa53521dd040f78449032a92f460349a67c2daf3606a80c2410487d42", size = 603338, upload-time = "2026-10-26T06:32:31.9Z" }, + { url = "https://files.pythonhosted.org/packages/13/e8/12db91dcc1b865d70556ba0b1a8128e850608612b3c7359b428e7118b177/adbc_driver_manager-1.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca595a044fabbb083d82c61c20ffea8614ac7568ed3d6978ac1c48f026e7789c", size = 616567, upload-time = "2026-10-26T06:32:33.49Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4a/60c6f39b9e66bf192efb3e2f593d8388bc056efd19288933494f7bc55a39/adbc_driver_manager-1.13.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17a0552d198105dd15b39a93d20e8eda79916e747d74cd49868282f447c8a88f", size = 4198546, upload-time = "2026-10-26T06:32:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/fa/69/d1796f2b18b4777b5571f102be9b7d9aa3371aa3253e8a67d79bb8d20d6c/adbc_driver_manager-1.13.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f851a4244edd2bf0fd33167cdb0d142963c089e53a720f42e550752b38c328c6", size = 4234979, upload-time = "2026-10-26T06:32:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e4/4e5bcf48779f9ea999d20ed3b622ce4f2481c144ae86d7a2a15dc4c883b2/adbc_driver_manager-1.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0cec917f7b46667e255842dc8d1f174125faac85951a7777527befd545fb4b6c", size = 778561, upload-time = "2026-10-26T06:32:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/68/9c/cdf0e92e23bb5cf8f24ad0ebaa771d6d4881ed9d1f0f3090faafdd562149/adbc_driver_manager-1.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:914e2dbb42d31763af97b472e23b99b5888681e8485d8931a776a4662dfae049", size = 934897, upload-time = "2026-10-26T06:32:41.345Z" }, + { url = "https://files.pythonhosted.org/packages/4f/60/b3d4c3680736f14e16f3511ee9306aad55349ee7d716b30e92e7205ca42f/adbc_driver_manager-1.13.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6c34be6fb94c81614ae2eafbf984428657cc8d694ee73503aa5c74f88a44e776", size = 583902, upload-time = "2026-10-26T06:32:46.957Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f0/3e5f67764da561a6f4389eb2a79d41bf3b3486361c47982fe0e50d0b39b2/adbc_driver_manager-1.13.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cad5bfa4f930c737cb2505c3888e091fa64bc2d628a3e2aadef307e5d50d47f7", size = 597158, upload-time = "2026-10-26T06:32:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/d488a47c4c489019c042f5622ed1f5fe798a96bd1db4e67c6dde1e326b5e/adbc_driver_manager-1.13.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68fcb9e8b12bb18171b775e389fea8228b315b344d7b9454a9c350e1cd17e28d", size = 4122336, upload-time = "2026-10-26T06:32:51.611Z" }, + { url = "https://files.pythonhosted.org/packages/39/61/5a1b64bb95d8712a1818e1e3fc67415ec8f930f7daa1f9f61c663f0b5bab/adbc_driver_manager-1.13.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb20b36d7dd23913e3bebd4b37499f4130c38a291150ba57205c25531782775", size = 4194847, upload-time = "2026-10-26T06:32:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/5601ebe623182360d0fede83b85d5fd3ec1cc1436f8bc566586d6984e0a3/adbc_driver_manager-1.13.0-cp315-cp315-win_amd64.whl", hash = "sha256:d961fc26536092500ef9f2333d1113e8954a67fcffb08148296b0238d35b4206", size = 762591, upload-time = "2026-10-26T06:33:06.227Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/4de47851ff5be7e20d590a89d667ad9a45709f70d2d95652835b9253b922/adbc_driver_manager-1.13.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5c59cb30aaff7bb1e4da95f6d92c4949532233174ba8d59b1b96102facddbfb", size = 921291, upload-time = "2026-10-26T06:33:08.052Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6b/53eb3b0df2735a7ef4d225589a0a4b59bb81f9b31728c623667dc3e1a234/adbc_driver_manager-1.13.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:1bb5a9787d3cfe88193b5d82bd866c3060c198fea3f36918797f4af80561920d", size = 601116, upload-time = "2026-10-26T06:32:55.66Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/4e91dc6dcf38c44e406efc47546e956e1d804a813de81d0a66626b89de3c/adbc_driver_manager-1.13.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:20231b8c38fef43719e3fc901a16f7dcf1cebe5869618242b31dca44879b78fd", size = 613341, upload-time = "2026-10-26T06:32:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/42a5738a5b23fd6629eb24e3410914b336c1e381858c88b7fb51bb1b0737/adbc_driver_manager-1.13.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd1d240663870aa4bf15efb4c789d8a71faa8a81fa8ee704975b0bfb2cad4f1a", size = 4189350, upload-time = "2026-10-26T06:32:59.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/081d2cd91b69ee3ff81a37c2a1036a2a7956586c8d784760d793892e7303/adbc_driver_manager-1.13.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5e85accd23f8665ba5381bbb99d655fa3728f4e998e1f3c8821b8f1b5476211", size = 4243900, upload-time = "2026-10-26T06:33:01.332Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/3a9377a8b7a649f06a97d0c657dfbf0c6bd7550cd608991ef7f7a7456895/adbc_driver_manager-1.13.0-cp315-cp315t-win_amd64.whl", hash = "sha256:424a902361f0ad8833437a726407f91c849173aa8dcf4b04de5a72602293bf30", size = 777474, upload-time = "2026-10-26T06:33:02.754Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/f152c3f58106370cd50ac6993109d0b265e93ce90b644d99c47e734d5e69/adbc_driver_manager-1.13.0-cp315-cp315t-win_arm64.whl", hash = "sha256:7b25f59a849b53391e22af1fd2a83d7ca36e171a89f52c88a240445764b3d694", size = 932449, upload-time = "2026-10-26T06:33:04.483Z" }, ] [[package]] name = "adbc-driver-oracle" -version = "1.0.1" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, @@ -163,27 +156,27 @@ dependencies = [ { name = "pyarrow" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/63/b4/1c0a418a4cd99a32ecf6d054d9ef71de869f7c4d83ff10fb6e953281f4fd/adbc_driver_oracle-1.0.1-py3-none-macosx_12_0_universal2.whl", hash = "sha256:4bfaedbb38803893791842601504e45d583ac5e94a75d3ca867e300f9322cf77", size = 4733798, upload-time = "2026-10-15T16:11:06.957Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e2/169358ad85ac78a2558bf9f458b70f1a6cb2397827f3f952e4a5016dff89/adbc_driver_oracle-1.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b9baeca7fdfdd04d1250f0dc2dd2932cad4fc2841bc737d18b5c04d6049c91a4", size = 8833896, upload-time = "2026-10-15T16:11:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a5/747b2bf253c3988a094f3619c7a54a77016a83964dfcabba085c3054e6f3/adbc_driver_oracle-1.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b1008012fc1bcd9efc11a593c06df9fa5fee4bf059ab7bacddb39e96ac3f24d9", size = 9641676, upload-time = "2026-10-15T16:11:11.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/a8/c7a6ba0cac3da4c2a0fdb12ed37105b64c789b20aaafb890408663920ca1/adbc_driver_oracle-1.0.1-py3-none-win_amd64.whl", hash = "sha256:7abc29e759119fafbcd6f59ab37175fcafd767c8ee9835a8fda73ee927a65e53", size = 9598563, upload-time = "2026-10-15T16:11:15.301Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/5febd3e8acdfb7b31110d3ab6b0bc7d349282b2dc7256e1867cf8d02f2e8/adbc_driver_oracle-1.0.2-py3-none-macosx_12_0_universal2.whl", hash = "sha256:12e2835459ec55d7f1705354d502b56c5f0153a5c367770b01621e53e3dfed30", size = 4351310, upload-time = "2026-10-27T13:58:10.375Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8e/209c8463211060c2cbc3e35b1629cb4ad32b83b4d1fd7099f22736beffc9/adbc_driver_oracle-1.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:373456f2d4fd41013e4c339b7cab5d5bdecec214312520415c2ba703579a2291", size = 8199440, upload-time = "2026-10-27T13:58:12.924Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fd/a463fd35656f587b1317cd2946795ce6fd7d0679fc9d58e35a3b91eeae78/adbc_driver_oracle-1.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:65b85a8a318ce9238dfb52e64f4aa68d8c475949f3426f27ab7c21ee6c2a5627", size = 9052381, upload-time = "2026-10-27T13:58:15.758Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ec/616046322ab021e2504d608c9867a13f6c674cd6d85470d8992184ba8fc9/adbc_driver_oracle-1.0.2-py3-none-win_amd64.whl", hash = "sha256:d47c9dc6fb7e7140e7d3c06975befda5d3f9f57ba94030e66364b5a81c137ab2", size = 9107033, upload-time = "2026-10-27T13:58:18.291Z" }, ] [[package]] name = "adbc-driver-postgresql" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, - { name = "importlib-resources" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/9e/cc757dfc1bb5472e35bf066ee4044f6b101bef61036512e8dfb4e97e7e08/adbc_driver_postgresql-1.12.0.tar.gz", hash = "sha256:766a002531bb99b691d2b92e7d928dea21c24ea567c03a6ee1edb61fe95b9187", size = 17793, upload-time = "2026-07-28T00:43:04.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/8c/34d8bb83444f7ee193ed8d90a59f263ffa1dffa65e4b49c64ebc556b0455/adbc_driver_postgresql-1.13.0.tar.gz", hash = "sha256:ecebbf32e4841d4874229614c98f7dcf60ec657882641d6cb20cd54b2a7423bf", size = 18532, upload-time = "2026-10-26T06:33:37.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ba/152bbe1d4a1cc13e2da72e76a5045ee25338bc75294f1ebf04c90b787287/adbc_driver_postgresql-1.12.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:28548d9e16497d2cb4750bc8e9e1abad3d0f981c7c0ff7afe70323f4b71c70aa", size = 3068434, upload-time = "2026-07-28T00:42:43.495Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d3/f17e69423ed7217b70155d8e531c5cf6fb74f3b598a583e6cfe541dc3e7e/adbc_driver_postgresql-1.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:03c617aee8796f38a0a2f1af50ceae92d40f0974f3abbe7eefbaf009fecdc5ce", size = 3337707, upload-time = "2026-07-28T00:42:45.568Z" }, - { url = "https://files.pythonhosted.org/packages/93/60/3b018e75661ac14a7aab7bb5cc1a95d72ddb9684abaee1e88a685406df81/adbc_driver_postgresql-1.12.0-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b523f15051b27eef18c3a822296c2d94b894be552a0dbe49fe14059e2c706155", size = 3822540, upload-time = "2026-07-28T00:42:47.619Z" }, - { url = "https://files.pythonhosted.org/packages/00/bb/ee19e7d56824c05892f82a3a2abca94fd2345b7de3dc22baac9e65a46acc/adbc_driver_postgresql-1.12.0-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c2dc9c29db07ba3e0caf293c57a7ab1259dd772d3725ff1f1aeedb7a1895dd4", size = 3512701, upload-time = "2026-07-28T00:42:49.519Z" }, - { url = "https://files.pythonhosted.org/packages/9d/02/7aa782cbb0134b09d1e67757c81332e0cd5e5697beecc8852468482193b0/adbc_driver_postgresql-1.12.0-py3-none-win_amd64.whl", hash = "sha256:5a3b5262eed6f28fb4c782b532e6a65caed1f2268fab7be736335ead49eed9dc", size = 3207946, upload-time = "2026-07-28T00:42:51.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/9f62b28e0a61e0af885e5e7271a78fc899dac3e9641c42aab01f7fe716bb/adbc_driver_postgresql-1.13.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:12fd64bf6ec16705d30d26c38ac2cc47fe8c7006ac2902c43234d61cb6ae8a43", size = 3076900, upload-time = "2026-10-26T06:33:10.567Z" }, + { url = "https://files.pythonhosted.org/packages/04/2f/79e0e9b471293320a79805a6dd77199ceaf8d73789dce35af5959b2db778/adbc_driver_postgresql-1.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7981ae7c5ecd7ba4c649cd00a96e81cf0e39475d3fa472901c5fd19b4e9de4a8", size = 3348673, upload-time = "2026-10-26T06:33:12.998Z" }, + { url = "https://files.pythonhosted.org/packages/a8/07/3c6c59a60fb423df8e5912f6442c1c2a0a966d5a0b8dc82de0807b8256d2/adbc_driver_postgresql-1.13.0-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2f3c51076fc27ba768bad2dc4cc3888655ac719473aa7d933b2813860491014", size = 3871372, upload-time = "2026-10-26T06:33:16.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/6b/dd22c016305e84b8b9959784e77bbd20c454bd82463dc8de5a839abde206/adbc_driver_postgresql-1.13.0-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a86819ba7c79b1d067c4a5344358b4d346bab58e888d7879ed2b25d97cc59ba1", size = 3560463, upload-time = "2026-10-26T06:33:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/519def2e78e435139de9de645933cfcfe38ca997193d5d23365406d228b8/adbc_driver_postgresql-1.13.0-py3-none-win_amd64.whl", hash = "sha256:c97a0b71e7205b7aac7f8bcd85a41280ef56e2d788110d2572c52ac3d447e25d", size = 3111598, upload-time = "2026-10-26T06:33:20.149Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/7e0176a984252e22f1428b8e9d5cce9b201ad521f424be40f2ac048fb37c/adbc_driver_postgresql-1.13.0-py3-none-win_arm64.whl", hash = "sha256:ed770fddf8b5fedf98e36b728224f1a5df3c0e1daaea1abc6feb41a765c7aa79", size = 3414650, upload-time = "2026-10-26T06:33:22.199Z" }, ] [[package]] @@ -205,19 +198,19 @@ wheels = [ [[package]] name = "adbc-driver-sqlite" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "adbc-driver-manager" }, - { name = "importlib-resources" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/02/2dc143bdd2a62c52d103d4b0ae491a347944aaed25b3d40fb11797750c70/adbc_driver_sqlite-1.12.0.tar.gz", hash = "sha256:18466a2f0c14f94cb0b17818157cc14ed6b93aef0a48ef648de945e9bac1540d", size = 12849, upload-time = "2026-07-28T00:43:05.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/37/539306d53a93c8ca2085daf211f21758e7ac9d96376b2324f14347e27e96/adbc_driver_sqlite-1.13.0.tar.gz", hash = "sha256:6c186aa0449eed95c361a97410a02c78c10619c4d209dc4070e77e769f8e779c", size = 13328, upload-time = "2026-10-26T06:33:37.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/f7/c35740269d3a5e3aa07b9ab155d4e943a7f5267f64d8f7396d5e14184a02/adbc_driver_sqlite-1.12.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2d5b3e9d0b5dbc66324b0ccf2ded886e3781f901be986892d319529b05536d3b", size = 1413592, upload-time = "2026-07-28T00:42:53.523Z" }, - { url = "https://files.pythonhosted.org/packages/e6/31/5d1d637e6ae76fcc57d5116d537aa78d2ab687354d5a0b2d527e085b61d4/adbc_driver_sqlite-1.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a81f53791e4aec69afbf8f77dac6acf48749fd84684e86601eafdd36d2eb7c3", size = 1357479, upload-time = "2026-07-28T00:42:55.194Z" }, - { url = "https://files.pythonhosted.org/packages/6c/99/415bf90eb912403d2d5d0c31baa1cedf200bd510f40027ee8fd3421c4c02/adbc_driver_sqlite-1.12.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c987d03e3f4850e57f218c8a0b9d224209123af642469ee1f36901c5a51725bd", size = 1501753, upload-time = "2026-07-28T00:42:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/69/10/a3156f19fadd254a4f58a328a8aa9472c981ff93bb4d23f3c22a4341796e/adbc_driver_sqlite-1.12.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3005a80bedf6624c6856da98037ea943a791aa8e82dad458259e0558be32912c", size = 1548999, upload-time = "2026-07-28T00:42:59.163Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d9/3245d741936100365ea77c434f84a0985523467bd812e5e11bb9b36d7152/adbc_driver_sqlite-1.12.0-py3-none-win_amd64.whl", hash = "sha256:0982bfc06158c2140b5c490b1a1325019c827b158f8432a30d49c8a0c18533ad", size = 1522964, upload-time = "2026-07-28T00:43:00.917Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e9/0dcdee1f5438c514cd7846398073e271a008b927f8337e273027482b2225/adbc_driver_sqlite-1.13.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:7044692c252985411f6ec4bf7947ff4e1b20f77c048f1e15438b2a4455941b79", size = 1422033, upload-time = "2026-10-26T06:33:23.735Z" }, + { url = "https://files.pythonhosted.org/packages/c9/93/a5ce6d47b993336fde99178f2cb8079e18d0c0edfacf9c68af5c34957643/adbc_driver_sqlite-1.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2003654d198b00cf3b9e72f0d7c2414afab2511dd00bbe0ec9a1980bf2ef0eb", size = 1366378, upload-time = "2026-10-26T06:33:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/974d9af8b401b141f00431bde238a36359d33e706edf8dbcf6b17dbababc/adbc_driver_sqlite-1.13.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fb6ff7adf05c01a9cd5ef5c62c926f6af6720f774e824829c7f648b801848af", size = 1525647, upload-time = "2026-10-26T06:33:27.717Z" }, + { url = "https://files.pythonhosted.org/packages/57/57/a4433a1c5e52d52358d390f5c99f5878d3fa1c7a1ff6618a40e13918901f/adbc_driver_sqlite-1.13.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fb51f307cb3c45aa5476c84eae83781079939832f35dd54611dbe6095627ca3d", size = 1573387, upload-time = "2026-10-26T06:33:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/1f/07/4e5551f023ad741cd6ba959bbd51d50dc224936cc1f20ca82560820643ec/adbc_driver_sqlite-1.13.0-py3-none-win_amd64.whl", hash = "sha256:87396ae95c1d0a10d95ea644afb6ae9843a64148a09c3e56fa7870ee03944a22", size = 1467059, upload-time = "2026-10-26T06:33:31.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/ef/cabba36d8cb660689fd1d043258fc8ef9d0e2d73721ebfddbb50b56b9847/adbc_driver_sqlite-1.13.0-py3-none-win_arm64.whl", hash = "sha256:94e0348eca2813a866bf553e2fd6121c179145f15817c8d18bff2752307378be", size = 1301730, upload-time = "2026-10-26T06:33:33.272Z" }, ] [[package]] @@ -237,7 +230,7 @@ wheels = [ [[package]] name = "aiobotocore" -version = "3.9.2" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -248,9 +241,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/95/4f7e0453d5e81bdb1b40ecf09f135bb88e12ce27fdb3efa125022dcca5d7/aiobotocore-3.9.2.tar.gz", hash = "sha256:8e32238c5bd77717ab1ac90077ff51ab141d025528115ccc7cc9b50b2d176e5f", size = 524343, upload-time = "2026-10-01T02:01:59.522Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/40/659f2ac3915d329e3e8e5bf674d35ce1774f3117c6f60339d5e05e505c36/aiobotocore-3.10.0.tar.gz", hash = "sha256:35480ef11c51e23087273b85d97f80da2f356a46ce5f3fb88e24483cd343ffdb", size = 530781, upload-time = "2026-10-23T10:44:50.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ab/c8f982c4dfaab49200e31820a4ffe6275356f7dcaaa1024735d8f8b4e048/aiobotocore-3.9.2-py3-none-any.whl", hash = "sha256:363b4892423b272eb84afbb58000da0be6ccd0cf8a68b98438c957d05b841efc", size = 102571, upload-time = "2026-10-01T02:01:57.883Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bc/3bb8965204ba51bbf4d021bc89d1357cd2179a17ef71b5fc429e21b511a2/aiobotocore-3.10.0-py3-none-any.whl", hash = "sha256:de0ccbcb82b6b0009f1b6667f48442e9d0432f9b8bfe6172fa51777cbea89d03", size = 102759, upload-time = "2026-10-23T10:44:49.109Z" }, ] [[package]] @@ -474,15 +467,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.5" @@ -516,19 +500,19 @@ wheels = [ [[package]] name = "arrow-odbc" -version = "10.6.0" +version = "10.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "pyarrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/2e/621da34d93b50666b0b096e3a61dd2118a31778181eb693955cd57c93e4d/arrow_odbc-10.6.0.tar.gz", hash = "sha256:02ab4dd902bb42dd37a104753f4224b745d4a4f437fab7a6d3182865c4f70c2e", size = 106916, upload-time = "2026-10-07T12:29:54.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/40ff0d4bdea53cfde441fd5d6d9b1d0e951ac8d862c4686c9e9a88d825db/arrow_odbc-10.6.3.tar.gz", hash = "sha256:056b15c47caee60c18827ad147e0c99a7524cf2e5c0887eed0fa32643d097716", size = 108856, upload-time = "2026-10-28T10:46:45.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/f7/d2f80aba2eafcf69eae6bee99cc171e663817041060ff70fd85c23cda354/arrow_odbc-10.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:94aba247e2300d4afdcfd8957c44fa8c64bdde4831ea4c91bb67038aeb3886af", size = 796971, upload-time = "2026-10-07T12:32:36.349Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f5/5022c69e7aff7524f0f391cef8e9c23e18eafbc27058940bc5447c460888/arrow_odbc-10.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6726932e3790b6448aea82df258eb1cf2d8e6c34045d15554da523ea3521d2e5", size = 773014, upload-time = "2026-10-07T12:29:53.261Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/3833166b4d19a75025b9cd4aee5b7d31eaf98f6cd460bcd04e6f0e658960/arrow_odbc-10.6.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1d8eedba30458ef1e5c22831d4d37df27b2bceddeff1ea106a51c54f1adcdae0", size = 882410, upload-time = "2026-10-07T12:28:32.103Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c1/72a3d03d66befec63b490d1533c9a2f3f4b7a12ef16cb99c8cd672d5211e/arrow_odbc-10.6.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:037f304b85ef825a16c01a0e53850be7ba2791f1f1085e6f80e0b67a38d30512", size = 914702, upload-time = "2026-10-07T12:29:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/86/13/591a07f4a77fa9ac6f6458905466ba5c80fd539eb8e1454bc9d8f0c9515d/arrow_odbc-10.6.0-py3-none-win_amd64.whl", hash = "sha256:9d05d7e8ed3f4af62d33790e3fbec190a0ead112ab898ddd3566843ae09954ff", size = 693289, upload-time = "2026-10-07T12:29:32.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/34/c834ef7c41956fa0157e6d5db53ee46667b1b383e90007cfe788a8ee3c9d/arrow_odbc-10.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b1c828a9378ad2711b876b030c029a1f4503932532530b12c52e16346d63ea7", size = 761584, upload-time = "2026-10-28T10:52:19.614Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/5fa28427d377fd6fcf57595c044186cb6c2f180e19d0256265e0d3e09dd7/arrow_odbc-10.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a10f461776a3b0d38af0f8fc20035053534786912e697380180f89835e9b8ef8", size = 744531, upload-time = "2026-10-28T10:46:44.051Z" }, + { url = "https://files.pythonhosted.org/packages/56/22/67d6b8f32ca3f5fcaf72fbb256b8e9c9802e7c77eafe3c83861c46387fb9/arrow_odbc-10.6.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6c2414e523ce8e157f3239e931467db8ab9597cc5a555ee81cd0fa0f933f6e20", size = 849241, upload-time = "2026-10-28T10:47:21.448Z" }, + { url = "https://files.pythonhosted.org/packages/63/05/5f61989017db4184bcaf813edfb4479fa1c79e82b6c6c6cc5b913c6b4a11/arrow_odbc-10.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6f9ed02f5ea72d7c5c9630816991ae297575ff7c54bd50cf556c91e1fb45be6d", size = 873569, upload-time = "2026-10-28T10:48:00.33Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/0648226a6e7d702b23cb6bbe585a68a6e251e2fbbc9814aba2858c0b972d/arrow_odbc-10.6.3-py3-none-win_amd64.whl", hash = "sha256:fc01d3e974329d88614b282249f40ea70df0d493cc82695991e086e374eee50a", size = 661175, upload-time = "2026-10-28T10:48:30.009Z" }, ] [[package]] @@ -597,58 +581,66 @@ wheels = [ [[package]] name = "asyncmy" -version = "0.2.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/a2/cf891f7c05b6292e0966c3870332d7778c14de912b33db4a895ac5151b9e/asyncmy-0.2.16.tar.gz", hash = "sha256:92a9c5d1ddb143783360b92f8abdc72612d7a2b2efb2a07482d2a816c9223be8", size = 94368, upload-time = "2026-10-06T10:52:58.263Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/f4/880a3392c756cf488ee60b656e57a8fee50252e469daf9d061a4d30a82dd/asyncmy-0.2.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd2016f01d67b4d8fe8ec04e2705c93740db3c6d111bdf4a15630116e2c6fa20", size = 2263246, upload-time = "2026-10-06T10:51:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/76/44/4313af9b1401f8c418a4f4cceea75467551e119ea52e6ef9bb8aa0cc8e46/asyncmy-0.2.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b36f27c18a349928242ecdcae101ef4ff130897038b7e7e6a6677f42a396129c", size = 2225909, upload-time = "2026-10-06T10:51:37.208Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2d/b28c7cd0a774c8e8f88466b9ab5992c932971bb4ee9923bf88cdff5c1d7d/asyncmy-0.2.16-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2feec5a05ea43eab2b9f3419208dfeace182d9a2291e0cb2a8a60e6284d72", size = 6899414, upload-time = "2026-10-06T10:51:38.422Z" }, - { url = "https://files.pythonhosted.org/packages/3a/60/0c33f36f1fcbf60a18adc31c993c6655c22de901f89a2c0dfe11076a5755/asyncmy-0.2.16-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e658bd49d94f322ebd36f7e687cc88972ec667b7b6f8dda29a78fb8da675123c", size = 6933200, upload-time = "2026-10-06T10:51:39.829Z" }, - { url = "https://files.pythonhosted.org/packages/24/86/1da36a00a1fe1faca1fe109fe878e9b8087f0c828af4e22b7861d31faad8/asyncmy-0.2.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b46824fea69b1cc6d94c15adbe351ecbfb2fa663ea50d61c6ca618f4bf92f03f", size = 6678147, upload-time = "2026-10-06T10:51:41.201Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2e/206ac3d2d7e08dbc43e78c4accfff54a5cfa7646eafe087e566f49fb945c/asyncmy-0.2.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bd3c8a94a646b0c28e97a599f25c327a9633a3c6738b7a7914869c758560b45f", size = 6771523, upload-time = "2026-10-06T10:51:42.956Z" }, - { url = "https://files.pythonhosted.org/packages/54/5c/a4d6db6c8429b7d161c77680224bc2bb0efdb8eac83db1367afd281697c3/asyncmy-0.2.16-cp311-cp311-win32.whl", hash = "sha256:ffa76b94895afdcfdd7f6043de2818dda5d5132ccd54a86f94801f163e760999", size = 2011557, upload-time = "2026-10-06T10:51:44.486Z" }, - { url = "https://files.pythonhosted.org/packages/af/70/d87838161b89a07cc4a21883e348e9e8d6eb0e294adc2842ad53a12c6a39/asyncmy-0.2.16-cp311-cp311-win_amd64.whl", hash = "sha256:7ec630f802c861f1300c4a30e30d294a1836f46271b820ff9b6b109588758db6", size = 2112860, upload-time = "2026-10-06T10:51:45.995Z" }, - { url = "https://files.pythonhosted.org/packages/33/b1/6cc46efe1d4693724ff5e76b50a60a78571efa1439133d0bb78ded8217aa/asyncmy-0.2.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0faad88c3c8fdffe3de6d626f58d2af47fa47531cb6d2100859b8fddd9685847", size = 2233315, upload-time = "2026-10-06T10:51:47.197Z" }, - { url = "https://files.pythonhosted.org/packages/21/72/a8b2e8feafcf3dadd48bd364ddc40d5d2125ffa1d3fd61a0fb715fcb553d/asyncmy-0.2.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20f148342baccae2a7995e745414f999bf116062975b7635bed9557895423681", size = 2212570, upload-time = "2026-10-06T10:51:48.588Z" }, - { url = "https://files.pythonhosted.org/packages/58/73/4fe290478d4898b5c34a46374e9c0604574f503d7d388d853710a4c07305/asyncmy-0.2.16-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f32ef4f8746a2b9073d63950be8a87466426da9bcbc8339943c62b4de34e70a1", size = 6787634, upload-time = "2026-10-06T10:51:49.961Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/ee3052e0b12737e1ea2293ac4b888f69c5a27c3c225a5054ba5e691091fa/asyncmy-0.2.16-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc5b0fba7feec70bfc0a4c571f2e0071e040d052f46447c491f28649a1b70c15", size = 6848958, upload-time = "2026-10-06T10:51:51.522Z" }, - { url = "https://files.pythonhosted.org/packages/76/d4/e1fb370a4dd2f9a295e1189f68afd975c6ad385056e9696e653ca76ffe6a/asyncmy-0.2.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6429983256fc41de0bae3782e2f89ed330b84baa2dfd398a87d9913b27c74620", size = 6505431, upload-time = "2026-10-06T10:51:53.286Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b8/c1d82f08f482272d06c2572645c0af13a2af2f2309b600ffe98dd2ab8cd8/asyncmy-0.2.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e0acb7aa6cea90f454df9be4fd5e402bea2d30d1d3dab8f70d48031e8627095", size = 6624495, upload-time = "2026-10-06T10:51:54.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/1a/9e0876385c282c308793619a6a05646918904d42270e6229a468f5c77fb8/asyncmy-0.2.16-cp312-cp312-win32.whl", hash = "sha256:c2798f09a62c4dad559951c40f8e89a87ad41758ad19376efe80e9dc0f1ac2d1", size = 1991358, upload-time = "2026-10-06T10:51:56.107Z" }, - { url = "https://files.pythonhosted.org/packages/91/cb/b5d617b87709c17f9de409eb55cbdce4c3c2849d8babe1c54bcc4d413557/asyncmy-0.2.16-cp312-cp312-win_amd64.whl", hash = "sha256:6dd4997a060a2bebe90ac8420e3b6a490b75f5c0a62cafbe7d19acd3f4c2fc9f", size = 2093206, upload-time = "2026-10-06T10:51:57.241Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ca/8b3d3fd98c68c0c244bafc3560b7869c0db98e46d4befb51001dc51befa8/asyncmy-0.2.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c16a1b3710b98077f1d2cf7fd54387b182a42abb2d49ea9f2dcdb41c46b77ee", size = 2235912, upload-time = "2026-10-06T10:51:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/21/ed/1e28cd1b6915670be596d266913773b8d2c4bac32516446a2d614225fb6d/asyncmy-0.2.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0431d9dafdf3a143674dbc22300d28ee42f82b30948430e870994a1f7d1700ed", size = 2212008, upload-time = "2026-10-06T10:51:59.681Z" }, - { url = "https://files.pythonhosted.org/packages/61/dd/086f85cc2a25e4d010bc0e34da9b4b43f433416b8f804a6fcc2f216bdbc0/asyncmy-0.2.16-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea88549833b99192612d23ce2678cda7cf3bd1c7c548b482d75d7de7be990f7f", size = 6724880, upload-time = "2026-10-06T10:52:01.193Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0c/d80c38f534b88c5cbc8937607b2facd965405bb84f790585ed07ec0a533b/asyncmy-0.2.16-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb9ef0552df7f3857cf58cbea9896fcc0f5db4cfbcc8d98bd89fcf2963f65759", size = 6787251, upload-time = "2026-10-06T10:52:02.478Z" }, - { url = "https://files.pythonhosted.org/packages/fb/42/0ebfc96405b03d77fc6b58930000f832107addec334b4c658b950572f9b7/asyncmy-0.2.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ed8a3073f03cfde57ea401181a97f818cda8eab85470c9d65591664fe9aa42a", size = 6454354, upload-time = "2026-10-06T10:52:04.186Z" }, - { url = "https://files.pythonhosted.org/packages/37/d5/86c165ff1dd47919feb71fdcdfd949edc577a1fb52f71862c7a789e09894/asyncmy-0.2.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8c08c47fd0acfa647a108d065236ff91f6f48cfdf618dfee7ade10dbfba8daf7", size = 6584637, upload-time = "2026-10-06T10:52:05.604Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/aac5a35ecbb4f8c8081c8c91486897a7b719d75aa9cc27b1489dac0cc824/asyncmy-0.2.16-cp313-cp313-win32.whl", hash = "sha256:74ae4c8a001bd041d1bcdbc5a72c63b204806a09327819a354f99c973499ccda", size = 1989029, upload-time = "2026-10-06T10:52:07.008Z" }, - { url = "https://files.pythonhosted.org/packages/ce/1c/0187d66ff58855d817616214c5220810f66d5070029773789dc0786af5eb/asyncmy-0.2.16-cp313-cp313-win_amd64.whl", hash = "sha256:091cdff819737e419e7e168d63f3df48d1ec77e196b8275b6b5ac4d19b2cb768", size = 2088793, upload-time = "2026-10-06T10:52:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/55/02/cd8513fc99ce4dc8c25c1c2a1f6d7cb74d64d107f23b3da6e5e5fa6e49e3/asyncmy-0.2.16-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e7fb933dcff03616dc36a7de9cdea85a67a1b2158684af3b5e6e0bd8858bcfdd", size = 3059809, upload-time = "2026-10-06T10:52:09.548Z" }, - { url = "https://files.pythonhosted.org/packages/45/5e/6cc381d7b8921466d1a2049b9a07e6a60420744200ea669c08eafbb1d184/asyncmy-0.2.16-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c79efdc3f6632b80c60900ae9605495a49bd0b81e586e7d837042d5dfd4d1ee1", size = 3028194, upload-time = "2026-10-06T10:52:10.804Z" }, - { url = "https://files.pythonhosted.org/packages/87/24/26bd110fc530d82f6f181f51562bda6574bca302518caf0ac0d050d43cba/asyncmy-0.2.16-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71504dd8d59cb912a84fb54cb3cf5aac094581875b6e53630077dcffad7d282", size = 12288234, upload-time = "2026-10-06T10:52:12.243Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e9/c14a947c437ee362e655826f5510ae0f42263bfe0deae825cd7943cda55c/asyncmy-0.2.16-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:594cee61496c840611f82c5b6b0607c19aa155442420d16b2c47f2c860a090bc", size = 11996856, upload-time = "2026-10-06T10:52:14.18Z" }, - { url = "https://files.pythonhosted.org/packages/14/f1/f43741a156332428c23e356eed3162015872d01a102f64d523ade3dba383/asyncmy-0.2.16-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80baaa4da31b64b57b0a266656fa4693f1a6c6c0f00ad1dd1e74f76dd9d280cd", size = 11636182, upload-time = "2026-10-06T10:52:16.126Z" }, - { url = "https://files.pythonhosted.org/packages/54/2e/f4158af50e6c38c9a4323c33a9f8f8e16850e7fdd7408a4c9501ef40ff64/asyncmy-0.2.16-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d1677191ba3faf318a7da52cad1f367ccea3301572ab49472e124ab962037f26", size = 11612894, upload-time = "2026-10-06T10:52:18.132Z" }, - { url = "https://files.pythonhosted.org/packages/88/91/4b3d6f18a0e27cbec4fa25b4eab4d5496ef5e6e9c58bf5418aa1e8a2c826/asyncmy-0.2.16-cp313-cp313t-win32.whl", hash = "sha256:f5f9b8484a63261c86322bad878b11a07fd4229b17557bdd72a38fad424b8ffe", size = 2556686, upload-time = "2026-10-06T10:52:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/be/17/e79d2c410c704a11e57bbc037407383c5cbf99b9bbad2733ba862568d7d4/asyncmy-0.2.16-cp313-cp313t-win_amd64.whl", hash = "sha256:9fa9c6d94f8887d89c65b1a3ca8899a1c580e4f0776136a5aa0d6240177d2650", size = 2755976, upload-time = "2026-10-06T10:52:21.011Z" }, - { url = "https://files.pythonhosted.org/packages/1a/30/1bffef5f0c961adcabb1846ffc83677edfbe0f04aa5b1825c8ed3b5f8506/asyncmy-0.2.16-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:75f4ad92c6e81e7e9660dc93d1720a5a318059304eb9ded112ca49dffa4f7ee9", size = 2242626, upload-time = "2026-10-06T10:52:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8c/d43362017e8e946f8ef28da3434a0105a4a33127cf367755553919273da5/asyncmy-0.2.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf36db8a319f1e1ca4facc0b55aa0521528ba850359e5b8120b2dd483e15cde1", size = 2224787, upload-time = "2026-10-06T10:52:23.291Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/a21ae6aaebeb5045c758818c4c6a605c426814fd70b8b6afa697e059add2/asyncmy-0.2.16-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3266def84b8b2ae6e71ff4ccaf1577e00030d0eec66a0c2aff0aa5589fdfa1cc", size = 6723050, upload-time = "2026-10-06T10:52:24.462Z" }, - { url = "https://files.pythonhosted.org/packages/2f/fd/3beee4e556e1f62014c64ef3784ad80eefdfa752d25dae842f28d099a799/asyncmy-0.2.16-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31674278284ab9054fc8b69ac24d99748338269949cf79dd7c8cec9bd0cd0c2e", size = 6736165, upload-time = "2026-10-06T10:52:25.846Z" }, - { url = "https://files.pythonhosted.org/packages/05/89/43fc5ac81887527ed50c532d3c6858dd9b4a97481cf00fa746da1eb515e4/asyncmy-0.2.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f4001c803c370ebd989d39febb8834fef4f66202549bd1e08513bd36d14df8c", size = 6467590, upload-time = "2026-10-06T10:52:27.172Z" }, - { url = "https://files.pythonhosted.org/packages/5a/3a/bd12f7ecc3be153d06ed8e42414ea3cda8a193ca703499b04fe15d17e8cd/asyncmy-0.2.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23884d17d593a1e1adc0d797a0c2778bb40c081b3ed951186f0798206cfa8e0a", size = 6520994, upload-time = "2026-10-06T10:52:28.689Z" }, - { url = "https://files.pythonhosted.org/packages/83/71/5dd22fe0484c7ccd8636bdbf8c4a7a381de51d6ec44aa118e381f674d7b1/asyncmy-0.2.16-cp314-cp314-win32.whl", hash = "sha256:fa5711c9f31c4f7061bdd508265a08b9770e87a64fbb0d3adc5314c4adef84b7", size = 1984266, upload-time = "2026-10-06T10:52:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/65/cc/b8d9a3ce3efcc860bddb8ada67af4b5f5a748fb64820c8a0ad17c95b5963/asyncmy-0.2.16-cp314-cp314-win_amd64.whl", hash = "sha256:d6bbb409f2829d9bca9a53599a9d8ef8429f7368d5b8ba30ecb8b13762e760d8", size = 2088335, upload-time = "2026-10-06T10:52:31.391Z" }, - { url = "https://files.pythonhosted.org/packages/01/43/e5f40d2959f508b5b0eae0f78a1e06f711480cf787b1cd127984c4c92fd7/asyncmy-0.2.16-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5c56c535960002fe28464db2803dc765f009793f5c159d2bdb27789d95822197", size = 3073133, upload-time = "2026-10-06T10:52:32.537Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/b1c16ce3bcc620d5ba6dcd8353b0ca1a42e9debd71de7d0d56b4ec525f49/asyncmy-0.2.16-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05b49abf8de143b7f809dc26116caf1d16a818510f6324ebc2d1b36edd3f7bf4", size = 3049620, upload-time = "2026-10-06T10:52:33.684Z" }, - { url = "https://files.pythonhosted.org/packages/58/fc/0083427f2ef6aa5c5d5be9dfcba2b33507b5707a481f8a545584a50f374b/asyncmy-0.2.16-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ae8bdb8a4dfae7c210a863aa1cff3ca467da7269d98d120501d0528081f531", size = 12297510, upload-time = "2026-10-06T10:52:35.368Z" }, - { url = "https://files.pythonhosted.org/packages/11/12/00bd8ae2e1b1a5a2993b9498b24d38a9889a52e5db33eb6e88347e5a9ff3/asyncmy-0.2.16-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e175a4286774a14fd9c5e9301882033583e234cf75b874e80c8025a439e2c4c7", size = 11958268, upload-time = "2026-10-06T10:52:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/dd/97/00c2270bdbb6a721c0038bc586f0c3733e3f223d1864b5342b9b9d95b48b/asyncmy-0.2.16-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:09c2e97cdddd68355aa9f26a22dacc06f48d56ec75778c614f130f32e6016193", size = 11660058, upload-time = "2026-10-06T10:52:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/55d74e719860d00846baaedf52cbfd619527eeaa402f249545a5cf14b021/asyncmy-0.2.16-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1246506141dd5d2782096118f2c76ccb2d332cbfd56f611e6c652def4feca721", size = 11567753, upload-time = "2026-10-06T10:52:42.213Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/11afcc252c161d7f3e6125c4dbaac42805fa90751d2af3f9ab7bf798db86/asyncmy-0.2.16-cp314-cp314t-win32.whl", hash = "sha256:ddc8b367e2d50bfaaeb1d00da260182f332fbb7ce420057cee69abd83f01f5ad", size = 2568979, upload-time = "2026-10-06T10:52:44.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/438b1a6c0bdb125b96dd8f388e053e2d66b7c723d7111721560e37d47976/asyncmy-0.2.16-cp314-cp314t-win_amd64.whl", hash = "sha256:e9a89971bd7f5aa743d8a7121b2cb4a4b82b85361c14e5770375693600add878", size = 2778482, upload-time = "2026-10-06T10:52:45.654Z" }, +version = "0.2.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f9/9ca78e8191897b1b42eaecb2585c3d88b67662a020146d6b0c908cff0082/asyncmy-0.2.17.tar.gz", hash = "sha256:a6b615e1062a1ecb943b75eb30dc45c54a9aaa618d85010f17822c77921e8701", size = 94746, upload-time = "2026-10-22T02:57:45.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/54/6c598fd913c33783996232b601875032421fa320f873a3e892cdf9c4e778/asyncmy-0.2.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f739450c5d107b19427c3e0688116019daff3695d57a24dec903edfc8e2d7b8", size = 2265759, upload-time = "2026-10-22T02:56:15.649Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/823d21ae673577475154b10f1fbde70d4c025075e4d044bf6da602984004/asyncmy-0.2.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efe8814f146fba5b9a05e63d87a17f484d0508b2290ade37a533bbcfb134bfe", size = 2228400, upload-time = "2026-10-22T02:56:16.776Z" }, + { url = "https://files.pythonhosted.org/packages/37/5d/cbefa4e5031498d7bd4a36078af1a9a39cfc47bde18a5d06046918b1ea16/asyncmy-0.2.17-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b250f2222e857fa26464826b7c84746a095184cfe0ef1da5fcefa2e65f52f816", size = 6907415, upload-time = "2026-10-22T02:56:18.003Z" }, + { url = "https://files.pythonhosted.org/packages/98/ef/213fbf8fd41da577d215c8ee1c5d5335ed2f17361f5a56a330efdacfc201/asyncmy-0.2.17-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:598ec32aa6dd186a6952b763f17fbb7ceb0fa394f008a85f49746c16229a86e3", size = 6939190, upload-time = "2026-10-22T02:56:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/68c7fbd531f065a5967ef0fbb3e845e9fe275506dbefdfb57e03d7d4cdf9/asyncmy-0.2.17-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe830a567d823d2219a4832d27a39298cc3844e0292acb88de18f57cc42e7c46", size = 6687418, upload-time = "2026-10-22T02:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/0b02217614867fab9428db058977820007eaafb12578972c7eda5f5869c2/asyncmy-0.2.17-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:898201715eddebf26b335156e1cb024bac83ff2331f25debdf75ba715cec205b", size = 6778721, upload-time = "2026-10-22T02:56:22.235Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/1071a3b8ccb822bbfc62cf3b5d9e1dc77362b99222516ae539620ba64b20/asyncmy-0.2.17-cp311-cp311-win32.whl", hash = "sha256:43fa09113289131a4c39734da042cc8095394e90f6231074482918442d4d1ac4", size = 2013751, upload-time = "2026-10-22T02:56:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/b5399cd780d4100385ae260401e803f2e740ce41065f1c3f2ad7f7c9dac3/asyncmy-0.2.17-cp311-cp311-win_amd64.whl", hash = "sha256:9826f6efee0c882288833b79851672825eec64cd321a0eea4e599009cd154cef", size = 2115136, upload-time = "2026-10-22T02:56:25.125Z" }, + { url = "https://files.pythonhosted.org/packages/81/d7/f4e1e37835314a69efa0233f99271a60c15723032bdbe4e5c860bee23369/asyncmy-0.2.17-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:749577dd5dd511ffb7bb8ca2f25e122276349ae78902ebf5090e745f4ff2bb55", size = 2235989, upload-time = "2026-10-22T02:56:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/70/09/ec8af1092ebed7c92a18817553b7142369d85bfc229496d4178262bb8a75/asyncmy-0.2.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:835d3d02edc913ae94dc64d0e78419a4eeda14f73d75e74242eea5616880673f", size = 2215233, upload-time = "2026-10-22T02:56:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/2b821755b27ce38a2fbb8bc48409cc33def23861023c131f45af1501e31b/asyncmy-0.2.17-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7a4a01b42b087232953542527961d5d6a1795b37a791c9ae33d89ac24d704c", size = 6791794, upload-time = "2026-10-22T02:56:28.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/4a/77936f41d3e4d3584a37f0e63779023f2c0fe31550cc77609f6651fc71a0/asyncmy-0.2.17-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eb0ba89f38f10cabbada24e37c2a0d25c6612210396c2a5a01029dce8110a3a", size = 6858124, upload-time = "2026-10-22T02:56:29.98Z" }, + { url = "https://files.pythonhosted.org/packages/17/ba/b762efcd343b5610a28f00ef90c7fb813f82016b75cd8d6eeb13057ba800/asyncmy-0.2.17-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e19501db949d142ce9281553282df6408a441156bc0a523320e22f0386a7a972", size = 6509117, upload-time = "2026-10-22T02:56:31.527Z" }, + { url = "https://files.pythonhosted.org/packages/ee/92/88be111f0e8600880c876407445766b774552e4fba70205b543d4be87c3e/asyncmy-0.2.17-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19ed009035fddb33d545fb2ab2acff78cc54ad8593de885eeb2a9b8caf73b1dd", size = 6634912, upload-time = "2026-10-22T02:56:33.033Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b5/676cd36e17149cca9c51572182ecadd42dbc62f4123dd69993e4fa209f8a/asyncmy-0.2.17-cp312-cp312-win32.whl", hash = "sha256:d6c1567b8e49ec2fede53ff62d670f5a6353efd332525a1f4f165dfc7607b3cd", size = 1993648, upload-time = "2026-10-22T02:56:34.805Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/bbd6bcc408977fd445a05499dd9700f3a7366f793dc585330f6fc27c2b86/asyncmy-0.2.17-cp312-cp312-win_amd64.whl", hash = "sha256:b788da2366a0d855ab58619fe9b88f69410e23f191094665703f3dcad1cc14a5", size = 2095550, upload-time = "2026-10-22T02:56:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/fe1113b37245f25eef4af688ce3770f8336f53261c10f27ba71d5e823d3f/asyncmy-0.2.17-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9183af6cb0a85f593b877afbbbec6981b1abd4fec0cc511d55c496fbf3fb5a9b", size = 2238479, upload-time = "2026-10-22T02:56:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/244b4ff95bb60ced6ec28544530dedd786834abf3a0e2667ea878524ec13/asyncmy-0.2.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b32d559b6aadcb16f57a32f8977f857033fb5082e2c89f10bf234d93225bf58", size = 2214565, upload-time = "2026-10-22T02:56:38.316Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/bc51846e9048d3ece9e021ca8c8dac046e118ecb321296fae7c888751759/asyncmy-0.2.17-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:734451670cff1a79069a051df49d25d4493e61d7e533ec6a906e2d083fde037a", size = 6732801, upload-time = "2026-10-22T02:56:39.59Z" }, + { url = "https://files.pythonhosted.org/packages/3b/60/ea19f1a75c08830db445ebe5478c5376f67d1e2bec68b5607a8dd49c6164/asyncmy-0.2.17-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4307e9b1090bb2152e1e0dafc50733302278bafb0f66310ad32356ebaba7eb33", size = 6796055, upload-time = "2026-10-22T02:56:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b8/77d5b234ae8398243e8faac4c512d73902ad566350f95472e910e7002527/asyncmy-0.2.17-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e8813ab22e007cc136a156ff5fdb990a957f97e6c9f7fe3a6582d369f6b53e4", size = 6460031, upload-time = "2026-10-22T02:56:42.211Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/63e1c4ae1f620cced28bc3628b2790dbf8987e268e3c3d0166449735ae68/asyncmy-0.2.17-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d761b7f2afb3198da404c60cd6f2bfd0744bc5fbc1d947d77807bb696b70c22a", size = 6592706, upload-time = "2026-10-22T02:56:43.916Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/bc94208464d723a0e3a57357e0b739386ff58a436c24e0bb2f74d7c1a653/asyncmy-0.2.17-cp313-cp313-win32.whl", hash = "sha256:30b111510937208ac22324d072ee0a35a7da69b3068918597bcc4358609133e6", size = 1991419, upload-time = "2026-10-22T02:56:45.128Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/47f164e15e7ca8223c7a5c7426e5cb7fdf0eefe8eb0718e398e8544e23ea/asyncmy-0.2.17-cp313-cp313-win_amd64.whl", hash = "sha256:9c7a1eea12b76e98fe5c96221d5a70d7e2c281cd839036462a7f372b37c5030d", size = 2091186, upload-time = "2026-10-22T02:56:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/21/65/484810632ef1466dd29614b6d09a9848017ff89893362820002acbb2f5fe/asyncmy-0.2.17-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:243b57105d97f19103d512ba22ec40648a6d2ae7140e0f307dff6769e8f4827d", size = 2245293, upload-time = "2026-10-22T02:56:47.606Z" }, + { url = "https://files.pythonhosted.org/packages/15/dd/f9fe4c883efd3e762bc0a3fde9697df3cdf0c542404487e3136802a1fa2f/asyncmy-0.2.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:876393715478830d0255be289227f0d2ab2b069df7c6f56b83f9d133146624e6", size = 2227552, upload-time = "2026-10-22T02:56:48.783Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/b7805690d22b955eae402a14fcad34783f65123e76d4fd4ce884bbe2b579/asyncmy-0.2.17-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d49bf017572f5d4bc91440be3d9ed66ae091d6c8393839618f7e8d7c63581c30", size = 6729426, upload-time = "2026-10-22T02:56:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/7283a8173b481bb3bf1641b680414aa9d55b001af22416b0240eb7d2ee18/asyncmy-0.2.17-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c44635e83e2d755c06affd4373ca51cc04f5ecf8a91e30b86bc9eebb9d1b0ef0", size = 6742737, upload-time = "2026-10-22T02:56:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/7b/99bc04e3ba4f892d80db4595a086ba848f0104d2d5ebe36f181dea330cf1/asyncmy-0.2.17-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:542f7652d8b27f989c646ec8ba0d67bd9620afff41038ba3668fb0c18bd0599b", size = 6472591, upload-time = "2026-10-22T02:56:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/de/c0/12eae0dbab2ece26c2da10f4e66df5fc13a94732281e7a3535fd1cdd17c5/asyncmy-0.2.17-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:11c5518baaa3e3b79689cc089ce3f07ab2950769191d657c03a8babfb15f61bd", size = 6530572, upload-time = "2026-10-22T02:56:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/ed/72/389a0a60ef487b7df23e85937c060206c39bc2cf6cdbaf990e1197ce5a2a/asyncmy-0.2.17-cp314-cp314-win32.whl", hash = "sha256:6add4c67840dd3f394db250b98f3a0766e600912012fd38767fa4ce93bd5b5ab", size = 1986841, upload-time = "2026-10-22T02:56:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/3a13c161b4bd1ee6f7eb193efa1b5b9a1f572281e397f1e1008de963788e/asyncmy-0.2.17-cp314-cp314-win_amd64.whl", hash = "sha256:b212f20323f5eb0604f645c0f4a705f6db36b94c3f2a842fc579bac95577bc76", size = 2090727, upload-time = "2026-10-22T02:56:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/57/3d/2fd032a4c666d8a52656366fef317a848ab9af1f1c6be68d30eea65d39e7/asyncmy-0.2.17-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:95610b4cd2f73b47cc0ad39d89f3f96c73f60505b8b08b58d91978f48bb5e94a", size = 3076795, upload-time = "2026-10-22T02:56:58.864Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/39e622c8d9d541df0740f7a77b2808724e11186f16126ce765e9e8746944/asyncmy-0.2.17-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:264720d5be4948e4661b7cc05f6a6e42dabbd3ffee422d49dc586a78c44be677", size = 3053252, upload-time = "2026-10-22T02:57:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/f09dd8940562deb3930ea3fb3c45c04ff090c4a6e556c2c1fe7ef175beea/asyncmy-0.2.17-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4d952a8f5879964b4544b883540ffc12d77f2d21c491bdff833ad11427dba0a", size = 12314597, upload-time = "2026-10-22T02:57:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3d/dd9081c9c619c73a7e7def9f1051af2dfc5cc2ae1de0c8ea0c8ce4db4e27/asyncmy-0.2.17-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37d86799aea0c4b34a9acf96f1fd3d185fe408899dbba8d22b072d0577a68f3", size = 11970826, upload-time = "2026-10-22T02:57:03.282Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d0cd968e392aa04ddbe7d2c2a7643aba921f3f0d6f95bb3923e71e5aa497/asyncmy-0.2.17-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a158eff4a9b88d8336f2a802b1400cfc71bc38bceff1361a56f4ac6d1531827f", size = 11674439, upload-time = "2026-10-22T02:57:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b1/07319f7be182687990ab83c9274f967983db306502a3869f7d34bd1b416f/asyncmy-0.2.17-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9caa1e6c3514e60f35f4ac91bc2631fe77adb52e52de305d3158209b1ba8b3d", size = 11580994, upload-time = "2026-10-22T02:57:07.044Z" }, + { url = "https://files.pythonhosted.org/packages/48/1f/0e6b6bb9650b2754e4e71389715471e71814f37c586704b0a07d1722bde0/asyncmy-0.2.17-cp314-cp314t-win32.whl", hash = "sha256:0215d7e577c5451ba5061e558cbdaf1a3f3e63589d530294b8aa25636ee96cc5", size = 2572323, upload-time = "2026-10-22T02:57:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/af/b7/438a021fabe31f372c1beb11224a730454bc81eee4413787a531fb5ff320/asyncmy-0.2.17-cp314-cp314t-win_amd64.whl", hash = "sha256:57f5f907f613bc1f2c49bb3ef07b07c0e0dfbc4427556abafef051d4f189fd55", size = 2780927, upload-time = "2026-10-22T02:57:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/fb/f1eb227d37b7470b3b4bb09c51ef1fab606886e5c4b955af4bc94d648075/asyncmy-0.2.17-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a5e073c5bab0f65d555b599f264a241ba7dd0a1ff6a098eb1d409ec876f226f2", size = 2245716, upload-time = "2026-10-22T02:57:11.712Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/c684dde3aa00474e9fea71ddfa4991eb472168bcdeb17d3c35048240c4e8/asyncmy-0.2.17-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0d564e78cbfce291b1b105d15a5401053f8133227b1aac630b4217870620749c", size = 2226027, upload-time = "2026-10-22T02:57:12.835Z" }, + { url = "https://files.pythonhosted.org/packages/19/5a/de500352d4fa9fa6236131e4fd1822c5121294a4dbecff0d3dfd8bf4dc47/asyncmy-0.2.17-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a763ad306eb7b7fe2ff1302100ca2215281b17850fcac9e64123206b4945188b", size = 6724746, upload-time = "2026-10-22T02:57:14.708Z" }, + { url = "https://files.pythonhosted.org/packages/72/14/3b7ea816d6b48997a0a6b7fae6aee41e7fcac7c84916600eb0a85ff1c267/asyncmy-0.2.17-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31879dd05d767b743e0f295735cb6470f92a84e42a3766cc7101fc97db1e6101", size = 6771063, upload-time = "2026-10-22T02:57:16.137Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/d76bdd36e21a9bfa5dd65eca7cdfb9815ff9ad68075fdaf35067104ef7e3/asyncmy-0.2.17-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ed7a3b8dfb10f2ff6707f2b14d30ad0dcc1a84a24333893bd7739c17c83f4ca3", size = 6469593, upload-time = "2026-10-22T02:57:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0a/5c12442264cafe934bee63a9734f27a8f814f53771a5ffee25c07306d0b1/asyncmy-0.2.17-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9951457ac149716c0780a043802e93e12f7f775b083a0c6c9332e84525c10048", size = 6545273, upload-time = "2026-10-22T02:57:19.151Z" }, + { url = "https://files.pythonhosted.org/packages/ee/66/3935e2de234bd17aa15ec2765ac99b25aded02b72f1941462cd62b5d3e80/asyncmy-0.2.17-cp315-cp315-win32.whl", hash = "sha256:33c0e5f404ea2d415407204051da243e3ae421ec9ab9d7f336bd57b381128714", size = 1985793, upload-time = "2026-10-22T02:57:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/78/0b/878988d0803ac8d7694581a3597cb90afaa5d52abb8346eba740a2664638/asyncmy-0.2.17-cp315-cp315-win_amd64.whl", hash = "sha256:1f977ba4d152d7268234eb61cff79c1a94a83af793fc5584d7b6f3967ea78d2b", size = 2089383, upload-time = "2026-10-22T02:57:21.637Z" }, + { url = "https://files.pythonhosted.org/packages/46/15/e6781b55d010f8d32c7c4dfcc1dd98636850c8c2e471ac2c209e99246460/asyncmy-0.2.17-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0a003bde5acc50c9f1eb050a3bd2ed183e66c1ff61e9897cdab749c80363fb0c", size = 3072447, upload-time = "2026-10-22T02:57:22.943Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9c/e3ca4eba5aa2a78627d8bea7b3501241af4e6b32793be15848bd1ca4a400/asyncmy-0.2.17-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:62a9f130533f087beae03810f8865af4d425cda792aa190f03687ac1235f2459", size = 3047069, upload-time = "2026-10-22T02:57:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/6b1b94c29afbf21b2301bd1996d807d6c472c98345b8e3778946ebc260d6/asyncmy-0.2.17-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d906092f2686fd55caed1e9e8ed267f1b9562d15b6bd4e07d62826a5b4b07d9", size = 12293960, upload-time = "2026-10-22T02:57:25.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/d6a21de5ca76c24e2c339423e9d3cae797db8f8bb0b9d9aab740e927efd6/asyncmy-0.2.17-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab098978004f34087dcd9949853619af58791f55731cc550d3e9af62c5b4063", size = 12208949, upload-time = "2026-10-22T02:57:27.659Z" }, + { url = "https://files.pythonhosted.org/packages/09/7e/f619042a40f5a34f46bb7b47b45289fcff8a23ae392a159b239862149ffa/asyncmy-0.2.17-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:505fbc81e136bd2805054887bd1ef537789f5e741d1bfd12081da0999097d563", size = 11661107, upload-time = "2026-10-22T02:57:29.589Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f8/7196da01e59d1b6ea16378a1d40692fe2f9e1670642dc0320b26f33099d3/asyncmy-0.2.17-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dd3d46af1bddc9601aac751fa39df7fe3f869a47f4fe7037b066fe9a66934844", size = 11745424, upload-time = "2026-10-22T02:57:31.318Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2b/fc65878ee5dd9257a9ee05b30fbb746cbc951c63a5c9c9969f9b0c008d7b/asyncmy-0.2.17-cp315-cp315t-win32.whl", hash = "sha256:544ce6607a52beffb19cfcf90ed3b005af822db7e0a91cf57a6b78802f2c7005", size = 2568999, upload-time = "2026-10-22T02:57:33.138Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/726aa0a8ade15cd51bdd04daa37fb64f9afbb1064ec43112355b8366bc82/asyncmy-0.2.17-cp315-cp315t-win_amd64.whl", hash = "sha256:74bc479969db2900f46405120bb5735fe183ab78960a6e3c6c05a1ac41505daa", size = 2773819, upload-time = "2026-10-22T02:57:34.311Z" }, ] [[package]] @@ -757,23 +749,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/44/a46089266de32a0d809023b6bf31b80a1f26a46954446a7ce695a565cbea/authlib-1.9.1-py2.py3-none-any.whl", hash = "sha256:8b9be8b1e5174dcbf5520e984f1a22dc2edae5b6e40a61d4cedbf86b97e9e69b", size = 266042, upload-time = "2026-10-14T14:17:50.695Z" }, ] -[[package]] -name = "auto-pytabs" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruff" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/ff/f5752f43f659ee62dd563af5bb0fe0a63111c3ff4708e9596279385f52bb/auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326", size = 15362, upload-time = "2024-08-18T13:02:28.437Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/df/e76dc1261882283f7ae93ebbf75438e85d8bb713a51dbbd5d17fef29e607/auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8", size = 13748, upload-time = "2024-08-18T13:02:26.907Z" }, -] - -[package.optional-dependencies] -sphinx = [ - { name = "sphinx" }, -] - [[package]] name = "azure-core" version = "1.41.0" @@ -794,7 +769,7 @@ aio = [ [[package]] name = "azure-identity" -version = "1.26.0" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -803,14 +778,14 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/87/e88098a024d74b7434de5b110c261da024fe5d6c139f1847f6f8a2b76678/azure_identity-1.26.0.tar.gz", hash = "sha256:61b4538b87df10d2d6d44b77cd5ea28a8929fd4929d58b45bf3f230f7daba02e", size = 310151, upload-time = "2026-10-01T14:17:47.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/33/0b881d0093d64c965bb20fcf71b806c7097881b544a2a55d519cdcf3bab9/azure_identity-1.27.0.tar.gz", hash = "sha256:6a4c50e25e8e9a47b324a95ad024897e56cb3c6241e0094e93b126a2bf71edc5", size = 331360, upload-time = "2026-10-26T21:17:39.095Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/d5/ae45ce8009d0d015304b206c7c4a6cbab02c03f8f07eedefb32b099ccee3/azure_identity-1.26.0-py3-none-any.whl", hash = "sha256:3c60d9682b3ac01507388d474cb5a5ffbe54152a96b6cdcaa7013da9315cf186", size = 195958, upload-time = "2026-10-01T14:17:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/6a2fe486576f41884a120ef7b2c3774f334f9143402fb73650c67db94ddd/azure_identity-1.27.0-py3-none-any.whl", hash = "sha256:92fd2a93c0d2f777dac6c04e5fd7d5734f6643582b983544ef66b4575844e46d", size = 203474, upload-time = "2026-10-26T21:17:40.92Z" }, ] [[package]] name = "azure-storage-blob" -version = "12.31.0" +version = "12.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -818,9 +793,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/ca/5299cedef5957dd838d4dc46f97bea37335bb39f6610d72b27e1a3317650/azure_storage_blob-12.31.0.tar.gz", hash = "sha256:997b393cfcbdc4b186d5911790d91f80387f7edc12c4d73eab963a2d26e5b2a9", size = 670868, upload-time = "2026-09-30T21:23:22.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/18/eb2ade6eede419d33493016ec3243ca9860f67d0bc71aa0461cda2cad0c4/azure_storage_blob-12.31.1.tar.gz", hash = "sha256:0d6e5b860a1357efc602db04cfa7c9950ac86126edcf7db89320cde281555fe5", size = 672417, upload-time = "2026-10-29T23:34:05.69Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/57/d1f45fbccc0dfbe6b6db7e5fa06199e35219c712f3743677bec1b4e7d78b/azure_storage_blob-12.31.0-py3-none-any.whl", hash = "sha256:0c0cb601d3462491d09ea96023cd791bb9dd4b173bf950daf3cff34ff47ba5b5", size = 455738, upload-time = "2026-09-30T21:23:24.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/b9f0dc477827887aef64012946bdba65e9aad67084ad38044661a72fe47d/azure_storage_blob-12.31.1-py3-none-any.whl", hash = "sha256:9e012d5f4ab658b7cb8a4581329d7a7aa2bf5f39b4d63baeefaae7f9e90fe6f0", size = 455904, upload-time = "2026-10-29T23:34:07.459Z" }, ] [package.optional-dependencies] @@ -828,15 +803,6 @@ aio = [ { name = "azure-core", extra = ["aio"] }, ] -[[package]] -name = "babel" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, -] - [[package]] name = "blinker" version = "1.9.0" @@ -846,41 +812,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] -[[package]] -name = "boltons" -version = "26.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/56/14c4a4931910a81ddeccfbe227925ea738e3c445d3e2af960f0bcbba1616/boltons-26.2.0.tar.gz", hash = "sha256:d39cfd15c1a1c3bd4d705c82252fa9edb8e4f5e8cc039f8e39afac7b1b47e92c", size = 242047, upload-time = "2026-09-07T10:48:17.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/ac/1f8e4f4855e382a67392ca3bba329cb93aba1e3039289c52a86bf337f7a2/boltons-26.2.0-py3-none-any.whl", hash = "sha256:41942cbce440211bbcf2487ccdc356964ad674f95a01068b8a0dd7a1d1fed036", size = 199536, upload-time = "2026-09-07T10:48:16.319Z" }, -] - [[package]] name = "botocore" -version = "1.43.106" +version = "1.43.117" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/b9/10ca68d0092895d5ea60f485a61a9840d5aff9d732c66ed60da53a20b1d4/botocore-1.43.106.tar.gz", hash = "sha256:006870b3b4e40547232ad12c3bb4faec91bbbe0659aafaa3b7fa48a112c4ee97", size = 16271399, upload-time = "2026-09-30T19:36:35.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/a0/d0a5c44ad6a01f195681ddcb4ce203f06c712811daf1d15f8f8785e8a9f8/botocore-1.43.117.tar.gz", hash = "sha256:71993702832cc37c2495048b9945f2929d306fccb3fb767e9964e437e637fa6c", size = 16366888, upload-time = "2026-10-21T19:29:59.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/eb/c51d3b54dfaa9267a687359d946dc2a1ca6a27fb4d381773e59b7bd72bf4/botocore-1.43.106-py3-none-any.whl", hash = "sha256:c1fb8818f9957cb5037f73db34ac1a12ba43562e31d6e7f9cb7e5fc64e1dba78", size = 15965506, upload-time = "2026-09-30T19:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/10/94/050e35c9febf6dd2fce4a76be587ae4a323964865700467d9d7b082f7133/botocore-1.43.117-py3-none-any.whl", hash = "sha256:5920ef9b3002e1361846549078549d9154a6e0004c22853c01ce7b279d05d495", size = 16061765, upload-time = "2026-10-21T19:29:56.001Z" }, ] [[package]] name = "bracex" -version = "3.1" +version = "3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/60/e2a6409041cb3f8ce7ac3d6917b5b30c39f81a0a5b2d414ec30f36fd96bf/bracex-3.1.tar.gz", hash = "sha256:35c69fc105f08d77c02722e8ae707c656ff298dd4d26c89bcb3c98fca8bc3614", size = 29040, upload-time = "2026-10-11T15:26:07.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/37/adf8dd2cde958c2d086bdd87fc36b532fd824bf38f10be6348d65da0c8f9/bracex-3.2.tar.gz", hash = "sha256:0a3d40bfae4caae4d3bfd0ea715874799fab711c38fd4012cc35343e815405cd", size = 29771, upload-time = "2026-10-22T00:28:49.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/79/c1ba46e1acd235affd36ce1d3346320ddcda663c9482a9e5f34727db1d11/bracex-3.1-py3-none-any.whl", hash = "sha256:52002b8455658c21f3b6e5eb0f662b57ec9085f42709e58bc86eeab5bc4c37d7", size = 11925, upload-time = "2026-10-11T15:26:05.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/8030b03de8137d1aab7dd6ecfecdba2fc22fc849fa35959b024f62293a99/bracex-3.2-py3-none-any.whl", hash = "sha256:8f3e908a452f66707e1d6d91b3d7d7d4c649d248df18537c0dc710aeb9c30d87", size = 12019, upload-time = "2026-10-22T00:28:48.233Z" }, ] [[package]] name = "bump-my-version" -version = "1.6.2" +version = "1.6.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -893,9 +850,9 @@ dependencies = [ { name = "tomlkit" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/7d/6749796426303b7a96868fa7ee63db6cd4e101ce0044430d60f5596fb833/bump_my_version-1.6.2.tar.gz", hash = "sha256:29d111e4767b28e8e49bfbd29f8e332c0b17e11f58089f630b877d623d4d541a", size = 1125306, upload-time = "2026-10-18T13:53:15.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ea/5ce9d814fbd532d8a5d8344bff688170c031f16ef11370b14b42fd3f8e7f/bump_my_version-1.6.7.tar.gz", hash = "sha256:ca21d720a4d951eb26354c9b62776a5481468c2319bd253c3494faf489f49c34", size = 1161983, upload-time = "2026-10-25T20:21:34.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/27/4c27b17e1ebb22ade659c5561fbdc573c098957f53cf34fb57e753b4e0a2/bump_my_version-1.6.2-py3-none-any.whl", hash = "sha256:76cf619b8e91b650b3bcd9e6058fb32da13a56ef9e9c8d786a0b2ed6e98b9ae4", size = 65755, upload-time = "2026-10-18T13:53:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/8a0bca33e613912312020e0a57a62e024a9fbc97a72dbc302fe7e3a6696c/bump_my_version-1.6.7-py3-none-any.whl", hash = "sha256:44e9f4fb733ff5d77f832249b7974d49a2f7acf3b566960265b5928788ab2f75", size = 68332, upload-time = "2026-10-25T20:21:33.587Z" }, ] [[package]] @@ -913,11 +870,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.7.22" +version = "2026.10.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, ] [[package]] @@ -1174,32 +1131,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] -[[package]] -name = "click-extra" -version = "9.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "boltons" }, - { name = "click" }, - { name = "cloup" }, - { name = "deepmerge" }, - { name = "extra-platforms" }, - { name = "tabulate", extra = ["widechars"] }, - { name = "wcmatch" }, - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/20/e2d60c90277a668b10a00f7a74cd52a87ab0c85afd5b8975cc582601eafc/click_extra-9.4.1.tar.gz", hash = "sha256:065df41b1c8f6601bd9e550d79ac34a73ba40bfc84cfdb320cdcfe066c40a6d1", size = 1759076, upload-time = "2026-10-04T20:44:16.522Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/56/32f36d2a5218c2d89c40802649b943f847162de8849850f6a26a88a23a83/click_extra-9.4.1-py3-none-any.whl", hash = "sha256:046c650c1bcbfd14d6836df64156891d18e73c23287ef7b4c19d839308cecda9", size = 665674, upload-time = "2026-10-04T20:44:14.372Z" }, -] - -[package.optional-dependencies] -sphinx = [ - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx" }, -] - [[package]] name = "cloud-sql-python-connector" version = "1.23.0" @@ -1217,19 +1148,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4f/1ab0775120965381a7feaff9655b3163b4aa5380655a4c66e6a80ea90b9a/cloud_sql_python_connector-1.23.0-py3-none-any.whl", hash = "sha256:cf3d0423ed2033ef1afebfa29acef284da460b261adc86dcd480a34941451102", size = 63370, upload-time = "2026-10-15T18:22:09.107Z" }, ] -[[package]] -name = "cloup" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/e2/d41446c6195eff0db3b671ddb202e39f42f9ea7c0dd15cd43fbf5cf0d7f7/cloup-4.0.0.tar.gz", hash = "sha256:83b0870ee863bcc85129e40e1b208bcfdebe4cd2142e9ce1d0daf7d276cab038", size = 252135, upload-time = "2026-09-12T03:23:39.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/34/20224fd9d52e8ce7e3b319b5d6b7126da25e89d00cd1a5692b4b46ed2d26/cloup-4.0.0-py3-none-any.whl", hash = "sha256:0678744a3a2753e1d78dd3ea5c6e4cbbc3b41561fd7a7a7d075dbc8dad74f74f", size = 55776, upload-time = "2026-09-12T03:23:38.564Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -1253,121 +1171,128 @@ wheels = [ [[package]] name = "coverage" -version = "7.16.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa", size = 971999, upload-time = "2026-09-27T12:29:01.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/fa/ce3baf63d85b730398d92a7162f486f3a5e4e2cc3382a02488b3943725ba/coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d", size = 223797, upload-time = "2026-09-27T12:25:54.756Z" }, - { url = "https://files.pythonhosted.org/packages/7a/57/9ba29c2aac7f756d479f03d45762120060f0f988788001001bf36e0e6fca/coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5", size = 224303, upload-time = "2026-09-27T12:25:56.214Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/0d6d60906dca7d28cc1e3fce12a9861801c4fbb6cbf220ad78cd059c9467/coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b", size = 254715, upload-time = "2026-09-27T12:25:57.755Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b8/9198b865679379fb165c689c64f6e11105ef380f6bd1c7673e83f73d9f5c/coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510", size = 256627, upload-time = "2026-09-27T12:25:59.131Z" }, - { url = "https://files.pythonhosted.org/packages/98/79/9521462cb6072fe394701bc8974b74afd576c9c9355156c7844e1a86a42b/coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25", size = 258724, upload-time = "2026-09-27T12:26:00.691Z" }, - { url = "https://files.pythonhosted.org/packages/a6/76/8d7d5d633db9fe0f3182fedc731bf09f9bcf2366055735152504ad614677/coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a", size = 260692, upload-time = "2026-09-27T12:26:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a7/76cb09c89ba46d74d37428bf93251fc14fb0bbe9e05cc2a5ef61773d318a/coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5", size = 254800, upload-time = "2026-09-27T12:26:03.369Z" }, - { url = "https://files.pythonhosted.org/packages/72/b6/2351c1979aaeb5b4a8091a75b90ca997ad60de36e181ddba267cf61dac97/coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6", size = 256423, upload-time = "2026-09-27T12:26:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f4/ad9a4f8b5cb2d494fa9452b546fe742ed2f9d3847cc14c05e36279a3e649/coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17", size = 254487, upload-time = "2026-09-27T12:26:06.082Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1f/a520470472f3e8b01169bf42162b1470c9ba992230432f62ca36269bf3a0/coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9", size = 258259, upload-time = "2026-09-27T12:26:07.513Z" }, - { url = "https://files.pythonhosted.org/packages/09/d2/ff26d5938274745855fa61cfcba0245c88ccc10d98d2cbd96064f16cd5a7/coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035", size = 254216, upload-time = "2026-09-27T12:26:08.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/5d832d3b06785d9f53267e4f2724a9f60c312eee6ebed9063a461d0d3b45/coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878", size = 255125, upload-time = "2026-09-27T12:26:10.35Z" }, - { url = "https://files.pythonhosted.org/packages/55/4d/1d33edbc2fcf7d99e384e393e712aa5a2ebbbd8409825357815982207976/coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516", size = 225898, upload-time = "2026-09-27T12:26:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7c/676df4882118756c4f8f560c954eddb93e166d84dda8c5f0b6a829689bde/coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d", size = 226369, upload-time = "2026-09-27T12:26:13.375Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0e/a457f4a461b3c5610d845137fdd45fa465e011a64c25af440518ab1f4e41/coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a", size = 226344, upload-time = "2026-09-27T12:26:15.127Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2c/f8296c63c5d542f3d21aed685e56b7031a419037d155bb3382fc0940d249/coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48", size = 223969, upload-time = "2026-09-27T12:26:16.753Z" }, - { url = "https://files.pythonhosted.org/packages/90/23/6f3dcb1423a0d43216e402ea1746e4a7c7c44f38896b97dd573790f56a40/coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99", size = 224328, upload-time = "2026-09-27T12:26:18.15Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7d/8f3b6dc920e3fc6732f7678785a2091db439f186afbec30dbf2214d9b1f7/coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808", size = 255832, upload-time = "2026-09-27T12:26:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/d1/36/6c45f15be4eca4ac1062c6a55a323286494c99726a7e58951fe85967ac08/coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d", size = 258571, upload-time = "2026-09-27T12:26:21.199Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/b54cbeba3ad89082c2e441278681859e538322cc34b84b2af7ebff00080f/coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c", size = 259680, upload-time = "2026-09-27T12:26:22.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a2/0dc65ec3d61930e1e4c2e371763b15eb4290896eb343a12d5d3091308116/coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800", size = 261945, upload-time = "2026-09-27T12:26:24.336Z" }, - { url = "https://files.pythonhosted.org/packages/d6/93/5fad7a61f2c14e08e98946fc31c1c7ffc1195061bf3fdc351db3be77a863/coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148", size = 256191, upload-time = "2026-09-27T12:26:25.89Z" }, - { url = "https://files.pythonhosted.org/packages/2d/47/74e5de9227b939ece9f64e729645ddc4296bea10dbfa98721c1333c8be2e/coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7", size = 257610, upload-time = "2026-09-27T12:26:27.35Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/2cf28d40b43645d1b72388fe3ee7f7c747533a6a9557bb8c24a7ae74fe1a/coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad", size = 255757, upload-time = "2026-09-27T12:26:28.949Z" }, - { url = "https://files.pythonhosted.org/packages/d7/3d/7c149fd99fc8bbc39c80db5e688d1d39fd040be2ecb78b8335a51a55b9c0/coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6", size = 259822, upload-time = "2026-09-27T12:26:30.515Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/b283fce09d5995e227bd8e513358dd7471bedc0f78abc85a925ebdb0a2f6/coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd", size = 255325, upload-time = "2026-09-27T12:26:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/bf/91/f3325edf0c4223fb1fe1532b8dbef2a1d2f729459a9a7d1a44d073bae534/coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206", size = 257191, upload-time = "2026-09-27T12:26:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/c4/89/21eb5e83ecf2eed523c4eb3d65ae513cd082c8fd1b6deb34c4cb6c332f97/coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a", size = 226034, upload-time = "2026-09-27T12:26:35.021Z" }, - { url = "https://files.pythonhosted.org/packages/db/de/e3ad6d864c0833624b4f1f9b53f9e58e116c945e5e965c3f1e172c5e84cd/coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162", size = 226569, upload-time = "2026-09-27T12:26:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c1/bccc58ebe5489cc70628f635c1932fd371f5d7da850dbcf960f95f4c4afc/coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02", size = 226372, upload-time = "2026-09-27T12:26:38.406Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f6/8eb4f220ef24f84fb27d852d4f9bf83e0c73ec1a4a08dd9a87e3f4529739/coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9", size = 223993, upload-time = "2026-09-27T12:26:40.164Z" }, - { url = "https://files.pythonhosted.org/packages/40/23/d4bbaf0c154e0b0c2b5264890dbf6ef098dcb50ec8f2469be9490d191660/coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b", size = 224368, upload-time = "2026-09-27T12:26:41.762Z" }, - { url = "https://files.pythonhosted.org/packages/7f/48/fc1e88fd571ec5cb38150b7f89f7696ca1bdf9920e01432febb69774cc85/coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204", size = 255358, upload-time = "2026-09-27T12:26:43.442Z" }, - { url = "https://files.pythonhosted.org/packages/1d/56/6785397d07c29c8e70fbb9a07e97d062b43c21ffc5f12385917847f09f63/coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5", size = 257955, upload-time = "2026-09-27T12:26:45.725Z" }, - { url = "https://files.pythonhosted.org/packages/27/3b/c8cdd07721e5f99abd81cea970d971997f99bf158c0b85f51bd284179c8b/coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7", size = 259183, upload-time = "2026-09-27T12:26:47.208Z" }, - { url = "https://files.pythonhosted.org/packages/9b/11/606b192fe43d32574ec6238549d48de588fdcc18485682a5ec0a8ac357f2/coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541", size = 261325, upload-time = "2026-09-27T12:26:49.084Z" }, - { url = "https://files.pythonhosted.org/packages/67/90/eea481f8b0305ceeb33f081a5f47e298391dbd1b589de0c4b3b3aa50d3f2/coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc", size = 255531, upload-time = "2026-09-27T12:26:50.509Z" }, - { url = "https://files.pythonhosted.org/packages/6b/be/dedbf9aea1457b120c27ac10b8fc2a357f37fa2b54c3e7286d42980a0a2a/coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967", size = 257329, upload-time = "2026-09-27T12:26:52.005Z" }, - { url = "https://files.pythonhosted.org/packages/fa/cb/b25c19d5bb2bd0f2e4e27fe8e2ffcae80c7a91ae181c0dc749ed60e9b1a4/coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7", size = 255294, upload-time = "2026-09-27T12:26:53.634Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a2/892c5c5f4ad44b7b2ca009aee705191f3f268f15052244f2f9e3539b2e35/coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23", size = 259444, upload-time = "2026-09-27T12:26:55.243Z" }, - { url = "https://files.pythonhosted.org/packages/ed/99/a562537deba0a3e370182ae71c149be796c39d8087365f17a09188f27145/coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8", size = 255111, upload-time = "2026-09-27T12:26:56.851Z" }, - { url = "https://files.pythonhosted.org/packages/2d/20/854ec68641a9b3362ff068a32dfa41637299761617ef253791dbade6fc76/coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4", size = 256882, upload-time = "2026-09-27T12:26:58.41Z" }, - { url = "https://files.pythonhosted.org/packages/db/0d/748e4518b0ac0f9ff2687c248a6e5f8c0737306e709372632a2556f84443/coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865", size = 226045, upload-time = "2026-09-27T12:26:59.983Z" }, - { url = "https://files.pythonhosted.org/packages/31/fa/6e46edba66a183fe4d99d4bb52c173287e9b8dddabe0888d24cb8210e580/coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6", size = 226581, upload-time = "2026-09-27T12:27:01.494Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d9/9ef6845367600b336ff75d000444a0d32497d6972c833141bd39356abf68/coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79", size = 226409, upload-time = "2026-09-27T12:27:03.116Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/577fc0803dab4155dcf808faffbdd7b159256781c0874a8586e17b81b149/coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a", size = 224138, upload-time = "2026-09-27T12:27:05.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/9e/e3785ba3ecba2bd11efc74bfe2801ca4b78c4480b15a375648d809a59da3/coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee", size = 224445, upload-time = "2026-09-27T12:27:06.805Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/963ff22d3fd27117da3b8cc442f5bdc91196f783321e1a8ff0ec43476772/coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b", size = 256112, upload-time = "2026-09-27T12:27:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/a8/d4/a306940c81c6ae759e82fff27d20b7fdc6896e422b821f51313cce212b6c/coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03", size = 258727, upload-time = "2026-09-27T12:27:09.927Z" }, - { url = "https://files.pythonhosted.org/packages/b9/a3/d3d99d93b02517087aa05bc0cf2d04d372956b849e5443e059079901429b/coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda", size = 259909, upload-time = "2026-09-27T12:27:11.55Z" }, - { url = "https://files.pythonhosted.org/packages/08/44/39dd599181726758dd185ae4dc0c0ab3aeabf7ca70e68e145060feeaaa16/coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c", size = 262481, upload-time = "2026-09-27T12:27:13.17Z" }, - { url = "https://files.pythonhosted.org/packages/99/e8/91ee43f6ded411460c359d7e1aebde4d6fd8f00a2e5394182d9d212eb23c/coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9", size = 256012, upload-time = "2026-09-27T12:27:14.91Z" }, - { url = "https://files.pythonhosted.org/packages/11/8c/e9499ddc33197bd7eabcb1118ca81756fc874457b324e2b479a4804b2ad2/coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4", size = 257898, upload-time = "2026-09-27T12:27:16.588Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6e/c081cb5991a0afba99f9c4ad6c74a5fce9513a38ddc64e3e6680c6fed9af/coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90", size = 255938, upload-time = "2026-09-27T12:27:18.19Z" }, - { url = "https://files.pythonhosted.org/packages/b2/42/1c3d819e8f9b6eb01c2fe90874d67a8882adb9507e0bbb09361ed131ea89/coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95", size = 260368, upload-time = "2026-09-27T12:27:19.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/4f/d70eac07901fd587b6ab05e659b52afe13959992aa5113bf6cce059cc572/coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8", size = 255620, upload-time = "2026-09-27T12:27:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/6d87af88317d3d9a9b18a9ca1bc1673eb516917f296e579d0d4a55cb3490/coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6", size = 257521, upload-time = "2026-09-27T12:27:23.358Z" }, - { url = "https://files.pythonhosted.org/packages/79/bb/90c2641170d2fa1a6757b3f8450ba2740197317b0ddd749e9604b914e886/coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011", size = 226248, upload-time = "2026-09-27T12:27:25.153Z" }, - { url = "https://files.pythonhosted.org/packages/30/08/d8d0478bb02c8eb0ae20a496fc80c40fcf4d3450bd184300d682ba2d28a6/coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b", size = 226732, upload-time = "2026-09-27T12:27:26.907Z" }, - { url = "https://files.pythonhosted.org/packages/32/3f/0001da22155b0a8ce063ec0f7e64ecbe17b373f306e7a74435f6d6accb72/coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb", size = 226645, upload-time = "2026-09-27T12:27:28.588Z" }, - { url = "https://files.pythonhosted.org/packages/d7/85/6d8813aff9b8b8586691a9d33c43c5604f7227622574da7cdc3d91a86861/coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf", size = 224871, upload-time = "2026-09-27T12:27:30.32Z" }, - { url = "https://files.pythonhosted.org/packages/5c/70/444f3a4981ac2cda40fdcf4cc9b56a4e1a33c222abeb33e51ed3e3eb2a6b/coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036", size = 225123, upload-time = "2026-09-27T12:27:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c1/980681cd7b33eb66ac835044116ef0a92e11fcc7bdd866cc89d10b1130b9/coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3", size = 265049, upload-time = "2026-09-27T12:27:34.085Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e3/87679875c33bb2191f0f05544a1cc9adcc940fe0c35443a10f2df753dde5/coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831", size = 267717, upload-time = "2026-09-27T12:27:36.025Z" }, - { url = "https://files.pythonhosted.org/packages/76/64/5d372776d6eb523d4e93bafba2253f96984e3b18261c4cc56a50863c6d0d/coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf", size = 270047, upload-time = "2026-09-27T12:27:37.96Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/44082ff0cbf9f97d0043f57970a71204097ec7ba606361a9fd2065393669/coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681", size = 271360, upload-time = "2026-09-27T12:27:39.766Z" }, - { url = "https://files.pythonhosted.org/packages/b8/17/9a215efe25b5e0ecc87c89dbe525c4a87d14d87c8c0c7316ef140a5f6f3e/coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0", size = 264736, upload-time = "2026-09-27T12:27:42.072Z" }, - { url = "https://files.pythonhosted.org/packages/a2/da/7f0a31af8e448107d4d32844bd684757f51ea907bc0c68c8fd537b2123ff/coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb", size = 267600, upload-time = "2026-09-27T12:27:43.85Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/3bfecbd3366b775bacdcb3330394d356cf384b5d8f5b2146ac4b14b252b5/coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7", size = 264767, upload-time = "2026-09-27T12:27:45.768Z" }, - { url = "https://files.pythonhosted.org/packages/b8/3f/5d62163732d87e4a0c4710a0eab30f0fd6a2d480112abe2029f014fe8c9d/coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64", size = 269026, upload-time = "2026-09-27T12:27:47.787Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/8e4579f225426535085a9be371cc75e3b026d058d679b80affbdfb4c3ef0/coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20", size = 264147, upload-time = "2026-09-27T12:27:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/d1/36/ef1f77e2c3f7bb03c2b13b9a2006f88700fdd75535ef158d70049f425c1c/coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce", size = 266376, upload-time = "2026-09-27T12:27:51.551Z" }, - { url = "https://files.pythonhosted.org/packages/be/79/0cb2bf4428830dec971c718c2c841a039c084415c99e67281f5a72841aab/coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46", size = 226585, upload-time = "2026-09-27T12:27:53.945Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f9/da17121c16667fd84998e972200ae226a41540f6ea4795776c6d99e8976f/coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c", size = 227378, upload-time = "2026-09-27T12:27:55.778Z" }, - { url = "https://files.pythonhosted.org/packages/74/89/01179c62d1b7e6e33bd5001566b02d7f778cf33d3ec1e81e94ca170c517f/coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91", size = 227064, upload-time = "2026-09-27T12:27:57.496Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8", size = 224140, upload-time = "2026-09-27T12:27:59.343Z" }, - { url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24", size = 224449, upload-time = "2026-09-27T12:28:01.299Z" }, - { url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09", size = 256060, upload-time = "2026-09-27T12:28:03.185Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843", size = 259106, upload-time = "2026-09-27T12:28:05.15Z" }, - { url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1", size = 260674, upload-time = "2026-09-27T12:28:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982", size = 263139, upload-time = "2026-09-27T12:28:08.771Z" }, - { url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34", size = 256591, upload-time = "2026-09-27T12:28:10.842Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6", size = 258708, upload-time = "2026-09-27T12:28:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4", size = 256562, upload-time = "2026-09-27T12:28:14.934Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0", size = 261077, upload-time = "2026-09-27T12:28:17.215Z" }, - { url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a", size = 256034, upload-time = "2026-09-27T12:28:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63", size = 258034, upload-time = "2026-09-27T12:28:21.242Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e", size = 226252, upload-time = "2026-09-27T12:28:23.102Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae", size = 226728, upload-time = "2026-09-27T12:28:25.21Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495", size = 226648, upload-time = "2026-09-27T12:28:27.136Z" }, - { url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75", size = 224858, upload-time = "2026-09-27T12:28:29.169Z" }, - { url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55", size = 225139, upload-time = "2026-09-27T12:28:31.131Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2", size = 265090, upload-time = "2026-09-27T12:28:33.04Z" }, - { url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2", size = 268254, upload-time = "2026-09-27T12:28:35.135Z" }, - { url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1", size = 270695, upload-time = "2026-09-27T12:28:36.959Z" }, - { url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f", size = 271853, upload-time = "2026-09-27T12:28:38.821Z" }, - { url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d", size = 265644, upload-time = "2026-09-27T12:28:40.815Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0", size = 268530, upload-time = "2026-09-27T12:28:42.725Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283", size = 265154, upload-time = "2026-09-27T12:28:44.684Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec", size = 269799, upload-time = "2026-09-27T12:28:46.672Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5", size = 265196, upload-time = "2026-09-27T12:28:48.996Z" }, - { url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c", size = 267266, upload-time = "2026-09-27T12:28:51.361Z" }, - { url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35", size = 226583, upload-time = "2026-09-27T12:28:53.33Z" }, - { url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5", size = 227374, upload-time = "2026-09-27T12:28:55.158Z" }, - { url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8", size = 227064, upload-time = "2026-09-27T12:28:57.075Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f", size = 215754, upload-time = "2026-09-27T12:28:59.075Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/79/e78bd8f75a9b8e32233ce92d4607f0a21ce88a9156cace2884e7175a82f5/coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712", size = 991021, upload-time = "2026-10-22T16:06:50.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/25/79a66272edd250031aeee6db87d500a0bf76390d0d5f8c2f431fe354715f/coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3", size = 225837, upload-time = "2026-10-22T16:03:55.622Z" }, + { url = "https://files.pythonhosted.org/packages/52/a6/f5f6fefb2fc1305363927edad1d0a60ce6ef80d6c1a74a79e825f0f670eb/coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe", size = 226257, upload-time = "2026-10-22T16:03:57.164Z" }, + { url = "https://files.pythonhosted.org/packages/28/61/72889bac8eb44c51f8043edb227df551dfdca1fc4819a6c6d67b3f69427b/coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea", size = 258282, upload-time = "2026-10-22T16:03:58.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/03e4a5644df9b73de0d44d1b82acd18b06c42b5abc3f1433be8363fa983d/coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5", size = 260506, upload-time = "2026-10-22T16:04:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4e/949022286df7e59e8633a3a7e6635e0e6b53af7d57074cb0bf8f500373fb/coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e", size = 262600, upload-time = "2026-10-22T16:04:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/7d67f60df081006315699b7edb9402c1b47142caf1383f4e56ae8ff1a501/coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7", size = 264609, upload-time = "2026-10-22T16:04:03.286Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/9293d94afbc099990a085e88229a51c79b00828aba0f2adda32dc4cc16d1/coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c", size = 258382, upload-time = "2026-10-22T16:04:04.804Z" }, + { url = "https://files.pythonhosted.org/packages/24/c8/bcd28efedf262c074dae76f61098739eb02b15f3f716f80cc86f9885b8a9/coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177", size = 260299, upload-time = "2026-10-22T16:04:06.291Z" }, + { url = "https://files.pythonhosted.org/packages/b8/dc/503f55e5e2200933403c90d619d9f779b5060ddd338b1bdc63cd235592cb/coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca", size = 258314, upload-time = "2026-10-22T16:04:07.729Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b1/f1a57c36077cc516240636f7eff0d1427939ed795f096dd2c7769213a56c/coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba", size = 262239, upload-time = "2026-10-22T16:04:09.116Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/c2e59d4a132936c7c207b9b69ab99c0f27b14c3c93266e9922aa3b562b37/coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3", size = 257841, upload-time = "2026-10-22T16:04:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/05/04/50742c0ee8b6c85826ff11737c93ed957fea4c9acf4b4b99804bdac1dd84/coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2", size = 259039, upload-time = "2026-10-22T16:04:11.916Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/83ad4bbbfcb6d045a33c14cdca313f6c9275737c67f3b7300222e3239a37/coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e", size = 227687, upload-time = "2026-10-22T16:04:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c8/dcc5a7effa79707eb8a61e699b141cff7c9b46036771274cfb48ea7b765f/coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2", size = 228290, upload-time = "2026-10-22T16:04:14.899Z" }, + { url = "https://files.pythonhosted.org/packages/d1/58/a8553e0dcd38d8136df63964638d2b588e0a4a916bff1bca996244fda693/coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241", size = 228199, upload-time = "2026-10-22T16:04:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/989fd3b495c4274ac0cf54cb6664326415e60183a7f8a73dd9bbb0b11e34/coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51", size = 225938, upload-time = "2026-10-22T16:04:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/cb/14726426bf3241657facbf0f7df7babd8f7d3fcf8e4609b7929f9dceecbc/coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40", size = 226166, upload-time = "2026-10-22T16:04:19.524Z" }, + { url = "https://files.pythonhosted.org/packages/8e/97/f95ed32cd3cfbe261a3598e694cb770cefc7b2d13ff013418d6f70a44dde/coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1", size = 258462, upload-time = "2026-10-22T16:04:21.079Z" }, + { url = "https://files.pythonhosted.org/packages/91/ac/eb4c31f741509a9ef48cf6f28385e98e9de0785599282252eee252ff8af7/coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983", size = 261789, upload-time = "2026-10-22T16:04:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/d9cd006d1a3cc5deab07beb29b86cb4a10dd84d66b330820363fb8d310d6/coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034", size = 262322, upload-time = "2026-10-22T16:04:24.94Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/4c872c6ae0762b09cdb8e856891bb3f584848cb60b6dda2bacb85fe2f3f0/coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b", size = 264600, upload-time = "2026-10-22T16:04:26.444Z" }, + { url = "https://files.pythonhosted.org/packages/df/98/1d41a2373fa98b423bcd70adb91d75e38eafb417955426dbb920b10016f3/coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e", size = 258678, upload-time = "2026-10-22T16:04:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/fe04b5e63e76506daa63d90bdfb00a53767fe5df18546b437d6226918d6b/coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5", size = 260210, upload-time = "2026-10-22T16:04:29.275Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/5c5a78dc19dd4799be9c6ec022316fb25ababaeacfaf0f52f24c56a57b43/coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5", size = 258375, upload-time = "2026-10-22T16:04:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e7/4c1d8eaff4d756fefd15d58c4953a78ab656782fd0e32c5a3465dba61aa0/coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56", size = 262422, upload-time = "2026-10-22T16:04:32.053Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2a/0b17c3e2e7b6ff1a5d92eb58915c0d1026668928f961d14dac73cf1b6096/coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f", size = 257875, upload-time = "2026-10-22T16:04:33.824Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/eb0cbde0535342df0d17a667163030a3afb67e5eb9e3ad94af9540041584/coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc", size = 260361, upload-time = "2026-10-22T16:04:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/1be2a6db7eb655d0a1d899217c3f2469f9121cbbeddac35c1c8173b1482e/coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e", size = 227747, upload-time = "2026-10-22T16:04:37.102Z" }, + { url = "https://files.pythonhosted.org/packages/11/c5/201c894c57de81875d8b8cc527ba222aa350e8cfb183eb3f6df45d06ad8e/coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69", size = 228325, upload-time = "2026-10-22T16:04:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/196095339b19758cf83f507fb598bc42820ae124745feb3084662729e17a/coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438", size = 228131, upload-time = "2026-10-22T16:04:40.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/1f01afbc9f4fa8ca5e1fa6fdf537a5c58b5765d1b4167ee284cddb9acc72/coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2", size = 227080, upload-time = "2026-10-22T16:04:42.325Z" }, + { url = "https://files.pythonhosted.org/packages/92/7a/bdb7e6260c4a211389aeac9b89dc31662ebe122153ad0ecfd156897912df/coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5", size = 227152, upload-time = "2026-10-22T16:04:43.737Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/276e7d5b3cfb124871bdb544c27b004a8b4e270e6bf8e7414149c54b5e5b/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398", size = 225397, upload-time = "2026-10-22T16:04:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7d/dac073db12f3c131598a6693253aded581e4d7d622a89864f11bfb0ca2f5/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb", size = 225705, upload-time = "2026-10-22T16:04:46.603Z" }, + { url = "https://files.pythonhosted.org/packages/00/45/877b0a9248e921080ae91d115c13192d17e308f5aee86bcc7f7bc2a54ed1/coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429", size = 225828, upload-time = "2026-10-22T16:04:48.065Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f5/2c48d47abec0e4590b302920334be5ee3854f96bbd22524feabb8d92b220/coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094", size = 226088, upload-time = "2026-10-22T16:04:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/ec0442f3f372c7fd742f49b3022edd392f79e37a0048e1fa333f56d01ec3/coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5", size = 257257, upload-time = "2026-10-22T16:04:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/eb0927ba93809f76eee69fa1e47999825fc7dbbabc3e0e1a2744f2677acb/coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669", size = 260165, upload-time = "2026-10-22T16:04:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a2/8d4c6055efb32099b15c9cc33d007b8aabebc4832f372a3274de5ba0afc5/coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd", size = 261098, upload-time = "2026-10-22T16:04:53.763Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/93412288688a7b03f152a734c8ef086e395108a0a6a746985d3967e220f2/coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722", size = 263164, upload-time = "2026-10-22T16:04:55.386Z" }, + { url = "https://files.pythonhosted.org/packages/3c/20/7eb8d805c4f1a212245cef20181dfbf39535357985073d49730ee9cf08d4/coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7", size = 257287, upload-time = "2026-10-22T16:04:56.976Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c1/1df29630b4ed17e135c76390afcc6bd273c3dc5ba2a21a76813a600fe98c/coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398", size = 259256, upload-time = "2026-10-22T16:04:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/a1/79/2365d1008d28aafe468a450a24c6c63363de794b5140223d67685cd0e205/coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e", size = 257178, upload-time = "2026-10-22T16:05:00.103Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9f/f002952d295d772688dffce0d991e3822795b1e3aad158073712a5a5e86e/coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28", size = 261275, upload-time = "2026-10-22T16:05:01.462Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/f9e15fbb426be97cd679ee5d359151e046f103bcd76b5092c318c26ed596/coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95", size = 257029, upload-time = "2026-10-22T16:05:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4a/3c398f38694387334ff4c9f71afc8e15b4fa7db7bc0bea4d5b6032eb0df0/coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9", size = 259109, upload-time = "2026-10-22T16:05:04.241Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a9/908a3cf1a2335aec1c788682a06c2f742a62a4f987033f080de1c5fd1997/coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d", size = 227691, upload-time = "2026-10-22T16:05:05.636Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/8cdd7bfd8a63b3e6b133fc395ecfc7b1cb6f561926e7e2ad050de85b83e7/coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d", size = 228418, upload-time = "2026-10-22T16:05:07.057Z" }, + { url = "https://files.pythonhosted.org/packages/09/cf/0b3fc57f735cd3831ca13d0be5312a5477d5934066683c5d34018cab7541/coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83", size = 228142, upload-time = "2026-10-22T16:05:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/722462fe79912ebebc101497bd16c42413cde2dbae771017f1ec0acb84c2/coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a", size = 227197, upload-time = "2026-10-22T16:05:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/826b9c09f2ebfea803bccb391d935688a25309bff8e819fc446bc629c108/coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062", size = 227314, upload-time = "2026-10-22T16:05:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/6d/19/c8514d7f5e0659a96ff2bc7377119dca6d420e2761c5fc69ae37618734f8/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6", size = 225477, upload-time = "2026-10-22T16:05:12.761Z" }, + { url = "https://files.pythonhosted.org/packages/81/74/ef9433ce599f07d8633b4ce6e6d6921505c4ef3f7f9006f8eb66e2c278cf/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95", size = 225787, upload-time = "2026-10-22T16:05:14.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/26e58981940e8b3944c6a9941bec5c7f50840e7009a046712e3f3bcd9e6a/coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85", size = 226019, upload-time = "2026-10-22T16:05:15.798Z" }, + { url = "https://files.pythonhosted.org/packages/e9/63/fe175310815cc8357b83b773c9b4c20943c269f217dd825a37c4b4bfe324/coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e", size = 226169, upload-time = "2026-10-22T16:05:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b0/ac84761f5b4bd226696b0ec4606903d2ef7208abda0aec06c14a4218def4/coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7", size = 258092, upload-time = "2026-10-22T16:05:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/e9/58/d8bdd65a661913bc12eaaa3e59742b2cb8b94f2e7f78e62184f0a58ca493/coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f", size = 261096, upload-time = "2026-10-22T16:05:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/5d82e2f0a75ea149e0a6412e137e1d610538772fdfd7199737db7fe234da/coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33", size = 262080, upload-time = "2026-10-22T16:05:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cc/e3ebfd295d1dd47c7bc84a17d73f150465a37ab388dd68fe82b495dc79a0/coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e", size = 264558, upload-time = "2026-10-22T16:05:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/e2adff76d6544bbc6df7366d74967923c2de37ba889a006bfb875bf5857b/coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274", size = 257855, upload-time = "2026-10-22T16:05:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fd/ecd1e9cddf1c36b5d03c438568b1f53e63db84bb3656539d71b454484c7d/coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2", size = 259933, upload-time = "2026-10-22T16:05:25.79Z" }, + { url = "https://files.pythonhosted.org/packages/40/b6/e17a19dccdbcad494c64691f71c1dba3e55c30625bc29ffbb88ac5a0aa8b/coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76", size = 257972, upload-time = "2026-10-22T16:05:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0d/37dac534709a3fa43a937c8b0604839bb44b8670b83d143a4b78fec20d1e/coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048", size = 262271, upload-time = "2026-10-22T16:05:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/7c05186925b4d64a46c271dfd944e3f0c1ad89db9095c31a6df157a75df2/coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c", size = 257604, upload-time = "2026-10-22T16:05:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/95/65/c241cdee66a0ff86c68f6d37328e735d10e3d30cd6ff66f356039eb688d2/coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e", size = 259714, upload-time = "2026-10-22T16:05:31.402Z" }, + { url = "https://files.pythonhosted.org/packages/db/09/e93233fe2405574ce0a31ed201c0cba3a860e085db5d2828bf12b7dc0790/coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657", size = 227914, upload-time = "2026-10-22T16:05:32.78Z" }, + { url = "https://files.pythonhosted.org/packages/d7/77/04979eb217edb94b94afabb4bcc179f60c746f68d3fd0b12688ba9195730/coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709", size = 228543, upload-time = "2026-10-22T16:05:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/a0/38/155f2f2c6769f8c43eca8a5e581443faa0908828650090b97103b4a41376/coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23", size = 228396, upload-time = "2026-10-22T16:05:35.951Z" }, + { url = "https://files.pythonhosted.org/packages/52/55/d6751572d9445485c4c5a51ec6ae11caa5c11b7ef2448cdccc884f7f54d9/coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c", size = 226486, upload-time = "2026-10-22T16:05:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a8/82d37760e537bfdb9fdee285fa1e1b45bc153293d23e2331c7befedd1990/coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4", size = 226861, upload-time = "2026-10-22T16:05:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/24/96/70ec00a9db6d7bdd3f914eba12057360769f5cb78a98456e71fe1937deb5/coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8", size = 266849, upload-time = "2026-10-22T16:05:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/77/d8/d99be3bfbb208512f039c80b31e404138a86678fc929a17acd5b7135a6ae/coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e", size = 269402, upload-time = "2026-10-22T16:05:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/6cbe8b5a5060edbd21ce8109c4e900f8b7656e8b72ede6dc4d82dc0b8bfb/coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764", size = 272025, upload-time = "2026-10-22T16:05:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/bd/05/d3633add01e5518dfdacfdbd1be41cb5c581412dd8bccd663edf6ef7fec8/coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88", size = 273515, upload-time = "2026-10-22T16:05:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/57/d9/b83387780271bf9ea9f96a3aeb01c1c302c4494a927923c4fa70cd170a3f/coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf", size = 266725, upload-time = "2026-10-22T16:05:45.713Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/8e80b29764ae3f4d70d44952eee3f3927eb36897490fe799e400adb7f5da/coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c", size = 269312, upload-time = "2026-10-22T16:05:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/e702f588fe8d83ca844f133270ad0a49bf650c8cf260551ff6f11a6b6bbf/coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b", size = 266740, upload-time = "2026-10-22T16:05:48.343Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/ee13ad14b714d75ef07c490b6e43969574a8b8b8c6d67bc741fcbacbf9c3/coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2", size = 271036, upload-time = "2026-10-22T16:05:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/e2/bc/854e8912c11c54aaf416e74602000589c6efbbe1f251dc90202fdabddcb6/coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c", size = 266158, upload-time = "2026-10-22T16:05:51.141Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/96432830ddf63bc42654de54eee77a0cd6949cc65769bda06f353323f23a/coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f", size = 268088, upload-time = "2026-10-22T16:05:52.92Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ee/230c6ec065a6629d41cc89323ceb54d37e2650c104f70fc108f4ee7c5051/coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa", size = 228229, upload-time = "2026-10-22T16:05:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1d/23eb6aadcb3c00764ff2961c0ff55f5dfa2543856d932b949bbc04fa68e9/coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673", size = 229154, upload-time = "2026-10-22T16:05:55.682Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/7df117e8e016b7a3ee459e833b3456f3610505afefd071f8727702d98439/coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74", size = 228726, upload-time = "2026-10-22T16:05:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/f9c24911755f91e6f451ca2e2c1804fbcbe9fecbc3310e341bbbbfa1d85b/coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559", size = 227196, upload-time = "2026-10-22T16:05:58.543Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a5/2b665b40c626e6bb40e5e8e23d91170b48b5f2f57186f46b07f2fac42386/coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b", size = 227315, upload-time = "2026-10-22T16:06:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8c/22b518d61b930b8a4d62905d046c76a03675ef19b466462011dfb16439a3/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c", size = 225490, upload-time = "2026-10-22T16:06:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/08/5b/d3b141455944e030cd4987f055f8c84a44a1a0b28d81ae93389f44051691/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f", size = 225797, upload-time = "2026-10-22T16:06:03.423Z" }, + { url = "https://files.pythonhosted.org/packages/03/24/aab2156983f7022361352640c583d08f0238e4cd7463eb921172001b4cfe/coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109", size = 226025, upload-time = "2026-10-22T16:06:04.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/db/94697b9ccf4ec94cdad07bf94e2a21b9702247cb212d2d5f18d2b9c3d0de/coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4", size = 226193, upload-time = "2026-10-22T16:06:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/a0/fd/db1c4ade4219831d590fff5aa5b7d9ceaf84586e8e60da6fff71b6182251/coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968", size = 258046, upload-time = "2026-10-22T16:06:07.51Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/84ed138990ed752abd73db4adffc1842d6152017d8e21809cc109a413fda/coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c", size = 261735, upload-time = "2026-10-22T16:06:08.861Z" }, + { url = "https://files.pythonhosted.org/packages/64/ad/c1e4367bbd1e7d562d6bc979c82b542f2040544e95a8cd995b694caf0e1a/coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771", size = 262819, upload-time = "2026-10-22T16:06:10.49Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3a/e8e8509b0940403eab15d86f70022790a01fce3e971a161919c544953f7a/coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f", size = 265194, upload-time = "2026-10-22T16:06:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/1f/77/d64bbdbb734b1059ebcde2b5df567f142a9948079e51af9b0abf8c9ec17b/coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28", size = 258410, upload-time = "2026-10-22T16:06:13.234Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/7cf81f08a254532c074eb52cfe6b12bb753d08f2997269c9fac8de359541/coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d", size = 260697, upload-time = "2026-10-22T16:06:14.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/56/bacea6dee2d5ccb31ab0cfffcee87ca7efa80ff048840209c8013aeaf02e/coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b", size = 258649, upload-time = "2026-10-22T16:06:16.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/97/fd8a6a63740f9ee177c53b59e2f4d0752369bb0fd154086a1e69aa1fb255/coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592", size = 262989, upload-time = "2026-10-22T16:06:17.85Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/7cd3d3520d45db3788456452b65446182c08f2a7c276f40c9dd11966be02/coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549", size = 257955, upload-time = "2026-10-22T16:06:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b9/80343a55e07d854927a915cc21a52e350ae1db9ead33f0248507b960785f/coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089", size = 260512, upload-time = "2026-10-22T16:06:21.055Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/e4c816f0cb3efdc6392bb276ff88359729f9dec605bdd3c1c7a44776f759/coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67", size = 227913, upload-time = "2026-10-22T16:06:22.407Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/283120142ac281834808584249b89957de997c28573ddd65ae106b622121/coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e", size = 228532, upload-time = "2026-10-22T16:06:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/b9/bc/83dbc6c3bd58042afaefcb0cfd7d7a2e3076989a2f3d9bc6e6edac819784/coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036", size = 228393, upload-time = "2026-10-22T16:06:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/00/ed/9463d1cb1766a2570343a372c2871c7fb4d561e5051f029399f4d57ce947/coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a", size = 226481, upload-time = "2026-10-22T16:06:26.563Z" }, + { url = "https://files.pythonhosted.org/packages/ae/59/6536c7fa87be13236b4184fc395f14d18f622a5b8ef7448a6bfdf5f24ec0/coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6", size = 226876, upload-time = "2026-10-22T16:06:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bc/4ffae6038f78382c9ab6a4969cb5fc01d50b083d119851085807629abe88/coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569", size = 267241, upload-time = "2026-10-22T16:06:29.356Z" }, + { url = "https://files.pythonhosted.org/packages/c0/60/cae4b708b4217d33ebf66b9c57f8910a257346b3eaf054a78d73605ddc4c/coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca", size = 270079, upload-time = "2026-10-22T16:06:30.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/84/1225ab04e55a6a2e483e903a5eb0f8f081cb5325df833594534aa288d91c/coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36", size = 272775, upload-time = "2026-10-22T16:06:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/ad/567f8dd9246a553aa8753732c41f0e0654e98e9a617d6282e64eae0e6aa8/coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697", size = 274110, upload-time = "2026-10-22T16:06:33.859Z" }, + { url = "https://files.pythonhosted.org/packages/66/5f/e0ee76895df40601e4aedffb6b8e71ea48e276a629fb4488b92cffffca14/coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2", size = 267775, upload-time = "2026-10-22T16:06:35.193Z" }, + { url = "https://files.pythonhosted.org/packages/19/6d/d19c6e45e940e5b9ba697b7be9a8e63cef73d90bdea45b670a59df466717/coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350", size = 270268, upload-time = "2026-10-22T16:06:36.597Z" }, + { url = "https://files.pythonhosted.org/packages/27/a3/b2731bb7aef2db800b330603675c3fa58a6497a05d5b5f8625f3b6285f68/coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a", size = 267298, upload-time = "2026-10-22T16:06:37.973Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/d0c90a81b6b6607933109914dc8b0ef0a5f4cacf9482ca868ae773ee5d0b/coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3", size = 271899, upload-time = "2026-10-22T16:06:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/1e/f038f3a2614e9ef839a40a184713aca40b8ed79e626ab5cdc388771045df/coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03", size = 267380, upload-time = "2026-10-22T16:06:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/8d636e639b2d8b1934128573402a7caf66c8cfbc794fa02061d84f115c34/coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35", size = 268993, upload-time = "2026-10-22T16:06:42.361Z" }, + { url = "https://files.pythonhosted.org/packages/73/7a/219f6b391cb0636242d06c390a8add5af91afa3e92afd0380dbedd34f487/coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802", size = 228216, upload-time = "2026-10-22T16:06:43.714Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3f/514d294319c5eac11494394545de209079f33993e14526c220eab29b2a96/coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc", size = 229146, upload-time = "2026-10-22T16:06:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/c0d8bb0a67df838fc7bc6f4060ffc404fd1154fd18e624d83e6abc41aeef/coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c", size = 228716, upload-time = "2026-10-22T16:06:47.097Z" }, + { url = "https://files.pythonhosted.org/packages/c9/17/48032919343afa2906112d458aceda2043afdb24a2ee8eb4c60904d8ddf1/coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d", size = 217383, upload-time = "2026-10-22T16:06:48.471Z" }, ] [[package]] @@ -1450,11 +1375,11 @@ wheels = [ [[package]] name = "deepmerge" -version = "3.0.1" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/6e/5cb3548b4d3112fea529375e55e6f3cdc52b8054e3a66f203b1f888ba885/deepmerge-3.0.1.tar.gz", hash = "sha256:35b39a4cb92cf328d6eca61cbbf65f68a37c2ceb3085f0f853cbb2e52a59fc23", size = 22328, upload-time = "2026-09-01T14:09:44.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/e9/5e99e56e03f9461858e686fdaf82e2516671bc1bd46f8b7ad7b9a8375f6f/deepmerge-4.0.0.tar.gz", hash = "sha256:948565f2bc9cdb41259a4a23d26638e5840c9e084a343ab279ffd3edcfdf74d3", size = 22792, upload-time = "2026-10-25T19:25:28.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/91/600003aaad107e27553fbc9cbfc57e96fa37e0223d2ef9d7d3a0e8d8d070/deepmerge-3.0.1-py3-none-any.whl", hash = "sha256:35c96f6a68fcf90719a5b31d9f8042ecef6c00fb56836660d33455d0f5cfda65", size = 14909, upload-time = "2026-09-01T14:09:43.364Z" }, + { url = "https://files.pythonhosted.org/packages/c2/89/362146b2ec7d0d503f824ac8496c88f0408ee0d39203b02cc10feac08146/deepmerge-4.0.0-py3-none-any.whl", hash = "sha256:a36dd5810fc3b90bd3e0b681158221361d6a31a6109411b27b211b371fed6d5b", size = 15429, upload-time = "2026-10-25T19:25:28.012Z" }, ] [[package]] @@ -1477,11 +1402,11 @@ wheels = [ [[package]] name = "dnspython" -version = "2.9.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/21/7fcd6780f61e9756aafaea760560509bb4111375daa2e23c1be7a89f33f1/dnspython-2.9.1.tar.gz", hash = "sha256:9a6d30e55f8dc63e6052597666112bff0586b95a2474a9deda03afbbf820d37a", size = 455302, upload-time = "2026-10-29T18:05:12.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/55/f90405bbbbd3dd8f788e92cec2d39964f5c09783f0570fc35f3bb3381485/dnspython-2.9.1-py3-none-any.whl", hash = "sha256:7c81caf8de1ed57b8cfc7e0ad4f528a0b8ceace473c7eb946df195dc9def751f", size = 360364, upload-time = "2026-10-29T18:05:10.206Z" }, ] [[package]] @@ -1499,48 +1424,25 @@ wheels = [ ] [[package]] -name = "docutils" -version = "0.22.4" +name = "duckdb" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/7c/dfdf5e9cebefd032f57f09b224993e861ecb55f61e36c709544b04b44f70/duckdb-2.0.0.tar.gz", hash = "sha256:09de511ef0dc76a5213e8d3d87a8b89bb4d3e8a6fe73a9f60dc001269946b44f", size = 16771077, upload-time = "2026-10-21T12:51:13.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - -[[package]] -name = "duckdb" -version = "1.5.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/0b/d65ea3be00ea79aa276a8388bec588a9cbf409ce637c6d306e5316210d15/duckdb-1.5.6.tar.gz", hash = "sha256:166a91dbfacfc0c9f08cc76c0243cb6d3d4296bfab5bad72a3cfb63140a5b7c8", size = 18032957, upload-time = "2026-09-28T13:38:37.978Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/e5/01e03d30b7ba33a030a4269fdca16ce445ce10f9d29b84a10fdbe0636ad2/duckdb-1.5.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c88700d0ee68ad149a0cc624df21b0f21efc136ea2449aaadd7cd0c9a564962a", size = 32757482, upload-time = "2026-09-28T13:37:29.916Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4f/7f7be626a4649a3948ca646c84d6afc1a00121f292f98e6f0d9ed68330df/duckdb-1.5.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e4f1b10a8b8ff476eb2b73955590fadbcef978da1167c593114c5edf763960", size = 17372997, upload-time = "2026-09-28T13:37:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/1a/66/9d57573729348d800a0eebdd508f1a833d3714f72e984fef79b47f0e6c45/duckdb-1.5.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34623eaabd2c66ba5c20f1a39486321c3b7d32e4e0e001ced95f81e3372dd361", size = 15514224, upload-time = "2026-09-28T13:37:34.467Z" }, - { url = "https://files.pythonhosted.org/packages/57/ec/97f595214b3a27b4ca42b8cab6d8121c06f3537dcc4d2da7bca0332de4c5/duckdb-1.5.6-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56c0f71c6bee982e9c30568bb12371bf66b26bf129c75d8d7f60bc69d6590a2c", size = 19428776, upload-time = "2026-09-28T13:37:36.689Z" }, - { url = "https://files.pythonhosted.org/packages/68/4a/ab59f4c1f76fb89e28d23f19b2729538e0723c8d328a07e1b8c37f9ee128/duckdb-1.5.6-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b108c04c932b36c2fa4e41110cc1c3c8cd510eb49f065f92d050be8e6929fd", size = 21537771, upload-time = "2026-09-28T13:37:39.548Z" }, - { url = "https://files.pythonhosted.org/packages/31/4f/9306c442ecad76f2a4d19f249e7fc8861f139dcf748315102eb69de8ca56/duckdb-1.5.6-cp311-cp311-win_amd64.whl", hash = "sha256:dda311932cf5aae955a53fe28a4fc1700c2ab5fa02dc1f165abdd5ec6c39141e", size = 13179009, upload-time = "2026-09-28T13:37:41.981Z" }, - { url = "https://files.pythonhosted.org/packages/a0/40/8a370e998293d3ebbbac4d926db30bb4ac5f700851a06ac31e7093bee386/duckdb-1.5.6-cp311-cp311-win_arm64.whl", hash = "sha256:df5ae02af278e084f54a9730a9f4f211ed736d0bd8f3bc12af925c2effb5b33d", size = 14046340, upload-time = "2026-09-28T13:37:44.187Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d5/d0ab77a0a1702a43171c93874f44c1f6481e30038bd3987df0d77a16a5c6/duckdb-1.5.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48d07d0651aaeac2c3974afd37599970154b7b79b54c18f27c319c14ccf98d9d", size = 32810486, upload-time = "2026-09-28T13:37:47.254Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cd/b22201de5377faa3be6c38d5f3eaa504cb480392a448bed6a4d2239469b4/duckdb-1.5.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79de3dfa8705b1ba0d59e7e3252e40ff399e0afd12f485502a6c7bf7c2fd809a", size = 17405278, upload-time = "2026-09-28T13:37:50.135Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/f9cfb1493bbdc2f095693a402e42dce1192077f9e11573f00baed6a748de/duckdb-1.5.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcccce20965e6986cd083fdf192c461685ad0b93cd1ccd0b2a8207f1185f078b", size = 15532943, upload-time = "2026-09-28T13:37:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/53/04/f65ccfaa5a833f2e570c4a140f03c8f95da416da9fe8ed08401f81f8242a/duckdb-1.5.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce89a1025a5317ebe9c520876c48032b5247ac574865486648b1a004f6009875", size = 19454940, upload-time = "2026-09-28T13:37:55.732Z" }, - { url = "https://files.pythonhosted.org/packages/4c/99/be75c788a492f8d77b7a1cdc1b19939ae7be0007f2028691ad371a1a33ee/duckdb-1.5.6-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc9619ed7d4ffa117b5155d84b44794366bb6635178d78ed5e13a6024845c757", size = 21568087, upload-time = "2026-09-28T13:37:58.191Z" }, - { url = "https://files.pythonhosted.org/packages/b5/95/889f8508960e47c0a7c75cc5bf57cde8512fc24f8db7b3129cca5388da42/duckdb-1.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:09ff51b230219f0d8b47fc8a1e17fb595ba9fab0c3d96a6de4d00b8ff86b3cf1", size = 13190189, upload-time = "2026-09-28T13:38:00.407Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c9/baab503364a68309f8368c88e77f5341e7d94927bdf3e6d703f0e5035f3e/duckdb-1.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:b8d795c8b2d5634b3269f974aa97f1fdf878f62f032317a52252a151b693fb1e", size = 14021977, upload-time = "2026-09-28T13:38:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/a476197fcba557738a588ec844747a19bc0a24b0e6f1809e308f29d68c0e/duckdb-1.5.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ae352646374cacf48e9981cf031191c494865192fc436d13667a2531fc5d1da3", size = 32810376, upload-time = "2026-09-28T13:38:05.148Z" }, - { url = "https://files.pythonhosted.org/packages/0c/6d/5466a2b53ddd557644dfa47a763f68748efccdf282e6ae7c4f1bcfb3da69/duckdb-1.5.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a1261e90785e9d29953293e44f60fa073bd1137098924e8de21a037a861b051", size = 17405385, upload-time = "2026-09-28T13:38:07.363Z" }, - { url = "https://files.pythonhosted.org/packages/d4/a0/bf87071170835ee4a34fe764fc11c1c6e7040a0e021b36c1b6f834a4c22f/duckdb-1.5.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97dd7a555b8f5298b76bc7d48a11cb2c64336e8de9bfde783cffb86ea9f54807", size = 15533132, upload-time = "2026-09-28T13:38:09.681Z" }, - { url = "https://files.pythonhosted.org/packages/31/e0/38095c8e140ecfbe847519ac07bcba94301b8fbb76b2870015e33e07f179/duckdb-1.5.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364992ba1089a2b327391cfcb68fd0bd0ce9090cf293baef861a0ba6847abfee", size = 19454994, upload-time = "2026-09-28T13:38:11.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/21/61dd2876bbaa69cf77d7b5c620e52e8b25faae7096f4d2e4a812b52095d7/duckdb-1.5.6-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:644f54ce99b3b61844bc9a3fe80e0aecb1ea4084b1fffc4396d1569db6111679", size = 21568700, upload-time = "2026-09-28T13:38:14.258Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/100730e7785e85268be4d4d5bd62cfc8314e261d2f42efa208243eef35cb/duckdb-1.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:ced693d33ddcee2e5345f077d342c87d2aaa80e41c514e64c9ff2d4e5963c251", size = 13190707, upload-time = "2026-09-28T13:38:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2e/bc7f44eab4e89ee5c1cb427bb1168ad021d985042e6841ec0694c3d3d501/duckdb-1.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:41ecc75bb9328d72d154a705c1a653d2c5c60f686a5c0c6578aa80020753c884", size = 14020962, upload-time = "2026-09-28T13:38:19.007Z" }, - { url = "https://files.pythonhosted.org/packages/fb/62/a8a30a4c6b94c0861d348ed5633b963f6745a5525527530f02f3c1a7c931/duckdb-1.5.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:aa21d2ad803b2524326e8622d7d96b2bb1ff1d5b60368e1978ee805df9c21fb3", size = 32828003, upload-time = "2026-09-28T13:38:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/71/b7/1dcca0005eb8c67adf9fc06bf0cbb1d2bf4ea1974cc89e7a7c2ad66aac28/duckdb-1.5.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a1b2ad27d414068cbca06c55cfa802eece10f86ea4812ff082f8ab4cb25fc85", size = 17413912, upload-time = "2026-09-28T13:38:23.915Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/e3ac175443550f3464f2d95731a8b0aae9b4dc3875c3a186c352262b43c2/duckdb-1.5.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c79c6d222b1d015cde73b5139087186b00db65357fb4e2c94c2308fbbf465a72", size = 15543122, upload-time = "2026-09-28T13:38:26.317Z" }, - { url = "https://files.pythonhosted.org/packages/9d/08/cc510a7952aba69d5cdca17f3ef61c95713d86143f2ee9aa3e097d38f50b/duckdb-1.5.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1052b8050ef5696e2c0d8c836949c72f3dd11f0690466acbea739613e8e2750b", size = 19457946, upload-time = "2026-09-28T13:38:28.877Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a5/6f8099d9a5a02ddff89e5c85875df3465054845b0920fb0703fbdf8dd2ec/duckdb-1.5.6-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c5e485e59613b8878d1670bcaa7a010f53c5a4da5ae8e08863e5e529ca6182", size = 21575132, upload-time = "2026-09-28T13:38:31.231Z" }, - { url = "https://files.pythonhosted.org/packages/9f/58/762f7159662d7859e201fa05ca29f306795daeabf84f3e087215a966b001/duckdb-1.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:ebcbd09cd8578ab1093393e9b16289cda0e8f1791ac595bf00eb5bad75c3cf00", size = 13713963, upload-time = "2026-09-28T13:38:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/64d165db322de13f5c3e75d377b6b9694df1821155ad1fa4b14b04601abc/duckdb-1.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:820a8384faef11cd86068ea48c5da57ce2d8f1c7b3d2bdb9be3398317a7c3728", size = 14514368, upload-time = "2026-09-28T13:38:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/73/57/ec7c1a01ab43fc13c55c17cc54a5740886e04ae830aa420ac00614b1420a/duckdb-2.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a3b38d8cdbef10007c64b65efc4087f799c4dc606785aa5f2a425ed13658ca01", size = 37006608, upload-time = "2026-10-21T12:50:38.611Z" }, + { url = "https://files.pythonhosted.org/packages/84/36/e1ea89acd74917ca654d5adc278765f0116e07540abf4134d5cf9f527363/duckdb-2.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:9bcc9921be0e1b2404b96a7fb447a8b80fe2f3a919cdc976ca6c39951cc8f2f5", size = 19691094, upload-time = "2026-10-21T12:50:41.839Z" }, + { url = "https://files.pythonhosted.org/packages/4b/81/2895b9ee3858dae7a57f791d7dbd4f116e9fa8fd3c71b9a377dbd4d292a7/duckdb-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61a244faae9fd787e30f4c76888769ec49e79736c434f698e8178a00af8348f0", size = 17603089, upload-time = "2026-10-21T12:50:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/04/95/0c8b5b95ac9e14ee83090a78048fbb23fb9f8abacd63e11a5eee7fcb31e1/duckdb-2.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcdf10dfbc2c1a6bdc5b2a94bba5b15481e552c5745c2af1994617b0eb6bc750", size = 21637946, upload-time = "2026-10-21T12:50:46.444Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a1/9c3780faada6e6a3edc5f9364cf19810e6e1ea90a7d5fd8fffc48b42c269/duckdb-2.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a3601121c7cc50d0b70412995634682fafcb58a0156d5d663dfd0b3bbe30a33", size = 24351483, upload-time = "2026-10-21T12:50:48.902Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/0729ac5d9857e5f0bde7262961f80e578a29945a444169886094cc9c69f4/duckdb-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a387d4907b535a4bd82ba8aede6809d0bf023dfbb21362832f3c9848aea7743c", size = 15370691, upload-time = "2026-10-21T12:50:51.59Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/392991ec9dcb22811c7029009184a910bd5cb69fbb7a626f36f151215535/duckdb-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:62bea9a1786be98d2906197c67a4089cc8b34d66d19e87e5543d8201e81323ad", size = 15731407, upload-time = "2026-10-21T12:50:53.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/28/1fb52f9ea10205a55a07c32e37cd4fd68fa9d1dc2520a12405ec9bb59cac/duckdb-2.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cb1cc30318397c3bcffa6b683196293698052588de3c1ba1e144ba0c72560ce9", size = 37003294, upload-time = "2026-10-21T12:50:56.489Z" }, + { url = "https://files.pythonhosted.org/packages/07/5b/fbe92644709fb9162cc51753f92f892b1f333658079d1f11efaed43341d2/duckdb-2.0.0-cp312-abi3-macosx_10_15_x86_64.whl", hash = "sha256:46c7a299ff5f45be9068cfc0ceecb3fd574af9535de8cb6908ae8b52d2846cbd", size = 19689207, upload-time = "2026-10-21T12:50:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a3/4d23938223626980cdcf18fd6b0dbf040823b5cf15beabbf4ab7ee276631/duckdb-2.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:b5d758b53751c930e09d371230241005ea5203c8edc65a1e1fafca83fcc906ef", size = 17597612, upload-time = "2026-10-21T12:51:01.818Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/985fb9d93d7f9f8e82092ddfe05ec8c9a1d0df7bdb0f562a10ced30e3e8f/duckdb-2.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e38daf0acbad9de7ce892d402b68409519291ee16af3c2cba85ace3b085c7eaa", size = 21655920, upload-time = "2026-10-21T12:51:04.36Z" }, + { url = "https://files.pythonhosted.org/packages/cc/33/01ad2535ee897db2bad70cbc800655167c53afd0b955eed84f0759c2f417/duckdb-2.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab51ec57d329d6f3df37c0287b55d71c3fc07bb54b61d1afed09104f953b1431", size = 24371005, upload-time = "2026-10-21T12:51:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/9e8ca1242b98f04041bef09686ecfe098b41f820aa23240f63f360b64eaf/duckdb-2.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:f61c96547ed432f54dae42a717fd88563eda55cb47549715fac9f61f770c205a", size = 15368860, upload-time = "2026-10-21T12:51:09.395Z" }, + { url = "https://files.pythonhosted.org/packages/37/81/6ea5dc1050f86ec4dd038a2774766b42247ba6104e7785f13a0c600e2b33/duckdb-2.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:c5e6708efaa8647e0914a16f1914354419e529a1925929950e7d610418e45873", size = 15726489, upload-time = "2026-10-21T12:51:11.643Z" }, ] [[package]] @@ -1566,41 +1468,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] -[[package]] -name = "extra-platforms" -version = "13.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/7e/b92eb5fe6c14b00c1469357622ee32e8b96ba8247b90b9da4cba3743ab32/extra_platforms-13.12.0.tar.gz", hash = "sha256:883feefe546402e1ccfe6f53b3f248dbaee3cd72d82e137549769db452c4712c", size = 494651, upload-time = "2026-10-12T18:13:30.702Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/26/7f61812ba2cc540edd3889802e90deeee03cd9569fafe41c64f1e35e03f7/extra_platforms-13.12.0-py3-none-any.whl", hash = "sha256:68a0cc9a16be81e6a9795bf413da7e14b1e8707dee260a47bedff7b6a5db58b3", size = 107721, upload-time = "2026-10-12T18:13:28.764Z" }, -] - [[package]] name = "faker" -version = "40.43.0" +version = "40.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/6c/b8793efc2f00a912ef17cf0b61b717cddf34607499efcb8a32238c119368/faker-40.43.0.tar.gz", hash = "sha256:02fae4327c03a4a6315e1b428a3878f435bfc276c93435ea349b95c0c9372361", size = 2032713, upload-time = "2026-10-09T20:06:29.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/22/965c7b822f4c80f6e003c696d5fa01b48eea6cc7852d61aec8779519b346/faker-40.44.0.tar.gz", hash = "sha256:97245a3756dcaf35418b89bea9f03ae6881557db0032bf048744c2aaee2a9108", size = 2034259, upload-time = "2026-10-28T17:06:41.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/ed/0d6d0d6467ae3d009fb82fb411867c7fcfeadbdd25602cab0d7a7963f400/faker-40.43.0-py3-none-any.whl", hash = "sha256:9dd7c0ddfaf30c842b05502d3cf641c135e0120a3a19047008ba8525b72953ed", size = 2069082, upload-time = "2026-10-09T20:06:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/58/9e/3da962809aefa00ddf457d1b11bba41d7766373a0ad9e537fa0804604361/faker-40.44.0-py3-none-any.whl", hash = "sha256:db808a3ec33fe9ce5caf40b9fb61f85f46e429a56966f765209272adec83e4c6", size = 2069747, upload-time = "2026-10-28T17:06:39.109Z" }, ] [[package]] name = "fastapi" -version = "0.141.1" +version = "0.162.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" }, ] [[package]] @@ -1675,11 +1569,11 @@ wheels = [ [[package]] name = "filelock" -version = "4.2.2" +version = "4.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/08/d8f81287742010e38daf3632380c8d80ede7b4df2716f8ca3d0a2b087d44/filelock-4.2.2.tar.gz", hash = "sha256:b29ff2ae9140bb17b3bc77c648dc4f010d9ac0955eccabff4f2b01dde859f787", size = 651339, upload-time = "2026-10-18T07:26:44.624Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/4b/2320d9fa03b65b4beb43b521fe1b12b7f87ab91ee42de3a286075016d205/filelock-4.2.5.tar.gz", hash = "sha256:37d8fa4f8b5e9233612e68ab23bf64cae41771322c8797361088ae2895b12894", size = 659943, upload-time = "2026-10-30T19:27:22.237Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/53/35698fff350cb99c49ee01ae5cb1eb4408567792a286314ea984b53b5002/filelock-4.2.2-py3-none-any.whl", hash = "sha256:1851d2f14ca2901e98069d68333b33ad7409f56904e17b48cc0c32a0db8ed73a", size = 162415, upload-time = "2026-10-18T07:26:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/14/2d/2cf36eca330b7a97c0e9076e8fc4272127c29398fa2bb7b24d4321b8b476/filelock-4.2.5-py3-none-any.whl", hash = "sha256:46ce42d27e5cdde09ee7197349663e97cb106679bd9e75db3a09679fbc7aa1a8", size = 163877, upload-time = "2026-10-30T19:27:20.409Z" }, ] [[package]] @@ -1837,9 +1731,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/64/bd0cd661009e1e379a7d5d100f13d40dd0a1a28fa70115931d0e0328f7bd/gcsfs-2026.10.1-py3-none-any.whl", hash = "sha256:037e7e1fa695f12719eeb778be6453085dce0ecd8b17ef1dbeecbbce05fa3b60", size = 88539, upload-time = "2026-10-16T02:07:26.913Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "google-adk" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, @@ -1867,25 +1773,26 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/c7/52dcd19f95d412a60b2e42bc4fc586f48106778d03de7c250840d6a40ba1/google_adk-2.12.0.tar.gz", hash = "sha256:93dfb7ff5e27d18f96520f59939faa8a7b6c21c7908161022702a3c208a37b25", size = 4305241, upload-time = "2026-10-13T16:42:58.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/2b/9616b2a498b8f247c1a9bef8c630e16151dd3a8ed768fa3118a961b19689/google_adk-2.13.0.tar.gz", hash = "sha256:bc2389c9d41bfadd9f0c87d6bfd8d5c7eaf1c449e8906079600b8f3752ddaa88", size = 4444119, upload-time = "2026-10-23T18:26:34.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/65/3706e52bac2f8ac1328caa7da28dfff8a9086fce5004619960f91c7f7bb4/google_adk-2.12.0-py3-none-any.whl", hash = "sha256:79f89ea94ccd87e4ac8c893e81c44ed2ce5568d05406a95c1d10bcef0ce0ff5d", size = 4998446, upload-time = "2026-10-13T16:42:55.26Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1a/6da4a8048b7cd47681928daed5d4d1c3ef971b3847fcc3cbc63f94df39d2/google_adk-2.13.0-py3-none-any.whl", hash = "sha256:bec4c4ee0aa3e63a2243c23365c5aef666ea539bc48c78dfbbf004dbddfc5f00", size = 5157034, upload-time = "2026-10-23T18:26:31.805Z" }, ] [[package]] name = "google-api-core" -version = "2.34.0" +version = "2.43.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/74/307919972c4dc84598f4128a8d6ae05ac3d9e6ca73fe60a7bb93c670931c/google_api_core-2.43.2.tar.gz", hash = "sha256:5dfc5e9d52982bcc03bd6af380a893365b5fdf907dc527f63d3a372833c33329", size = 286250, upload-time = "2026-10-29T17:20:40.193Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1d/a0120c7d937597293fce74510c80fe0cec8e45b1d702a816b567b6754c25/google_api_core-2.43.2-py3-none-any.whl", hash = "sha256:5a9d21baab97fd3c0547ccfc186a9e64f4227d8d7026f393835316400b29a6d4", size = 224304, upload-time = "2026-10-29T17:20:17.733Z" }, ] [package.optional-dependencies] @@ -1965,7 +1872,7 @@ wheels = [ [[package]] name = "google-cloud-bigquery" -version = "3.46.2" +version = "3.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1976,9 +1883,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/df/1b6f6fa4c119babcbbd002826a0a24ac287066808fbc873e7733b5ecc2bf/google_cloud_bigquery-3.46.2.tar.gz", hash = "sha256:a87e2e8026133e709deab9ebf537a6a2eee3fbcf4befa334a2f9f6164435b331", size = 530538, upload-time = "2026-10-15T17:49:37.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/96/482bde8e8b87b65f80aa9c0a4fa86e6e92ae407b6f50da574f264b923b64/google_cloud_bigquery-3.47.0.tar.gz", hash = "sha256:bca7a5ff52326172dc8c292e4592a5b46b491b6872010d242d7663d8ea89b12f", size = 553095, upload-time = "2026-10-29T17:35:33.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/ba/e89552c32a1cd5483261483c303da560e6504365d7314ecd88216ff9e119/google_cloud_bigquery-3.46.2-py3-none-any.whl", hash = "sha256:5e9a4e15f778cc00ee88167de9934a4c1bbb7fc20058e8f573f2bfdaf0d99052", size = 269195, upload-time = "2026-10-15T17:49:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/48/79129b85526710828e84e83cb66efad3680587ac63dfc30ae6eb7ab2fb7b/google_cloud_bigquery-3.47.0-py3-none-any.whl", hash = "sha256:4e42781c3aa7acfc1d3a91ac4052aafcbc05288a4cc1796550f4f94cb9ee3aba", size = 275370, upload-time = "2026-10-29T17:35:31.888Z" }, ] [[package]] @@ -2054,7 +1961,7 @@ wheels = [ [[package]] name = "google-cloud-sql" -version = "0.1.1" +version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -2063,9 +1970,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/e7/1fadfb0ce27e040427d763865a3d75db79ff3e994983bbf680344c5bbe7a/google_cloud_sql-0.1.1.tar.gz", hash = "sha256:58b56e81698faa3ff9ca551bbb29bf0c947f79a488bf0b0a5e33c538ba244491", size = 1084532, upload-time = "2026-09-02T22:23:20.604Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/8d/15f6c63fe83d53b6f1a0108d1e41c68c7ea42cb9c85bd7ad3e3a222afb26/google_cloud_sql-0.1.3.tar.gz", hash = "sha256:3140a6fd82b002cd2c1605d4985de8d0a1a42013d358e87e8d21b2b0a5b3a7cc", size = 1260732, upload-time = "2026-10-29T17:20:51.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/26/d239e6550ec996712a21d9f0db798624532172cef82f5c45a493edcc832f/google_cloud_sql-0.1.1-py3-none-any.whl", hash = "sha256:6df562d0cacf73fb503cd51ac1952497dea9c2f58187f60a6d295d5accf72ec9", size = 1096495, upload-time = "2026-09-02T22:23:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/54/f5/df647f08a3e06f9beccbf5a92fb2553350455d0cb0f883afda169c68b39c/google_cloud_sql-0.1.3-py3-none-any.whl", hash = "sha256:c0bdb1e4aa660f00860272131f73bc1089a52fcdb8a7660a7e02235598976fc7", size = 1263464, upload-time = "2026-10-29T17:20:32.421Z" }, ] [[package]] @@ -2087,7 +1994,7 @@ wheels = [ [[package]] name = "google-cloud-storage-control" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -2097,9 +2004,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/8b/71e457c5442faf669fb23c3b400952d2f2de4d7a5cd576e52275b25365b1/google_cloud_storage_control-1.16.0.tar.gz", hash = "sha256:52af754aa24bee34bc58287f67a1a849b07c33a010d58d2c1f7c91fa1be0f119", size = 160170, upload-time = "2026-10-01T18:18:16.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/92/3f294c663d28081f48e5abcde340e300556e679d6edb4f10392b277d9c7a/google_cloud_storage_control-1.17.0.tar.gz", hash = "sha256:b49f4d9e2d43e18cc6760e37a99ff5d9f674f4532e43e6d84fa69ec194c82bbb", size = 160368, upload-time = "2026-10-22T23:43:31.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/6c/c779f90e89a381ba38c3bd4173f1560e0765dd4d2b31ca686f7c90484a64/google_cloud_storage_control-1.16.0-py3-none-any.whl", hash = "sha256:b3b20047d09743fc143db70ac2738f1df7c482af381335bb0dbb86ee4732215d", size = 115201, upload-time = "2026-10-01T18:12:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/9bddf6ce0b0fe808d420c737ea83b788f6be3d393058e74b1cc9e59f0529/google_cloud_storage_control-1.17.0-py3-none-any.whl", hash = "sha256:ed313e855d0d65810181ab9134882b474d5f7583c13dca723765aa0e993127a4", size = 115217, upload-time = "2026-10-22T23:43:03.797Z" }, ] [[package]] @@ -2137,7 +2044,7 @@ wheels = [ [[package]] name = "google-genai" -version = "2.31.0" +version = "2.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2151,9 +2058,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/69/6a3d3839ab56ec17d7923c8679b38955e286e0a9793df10ce6d13bc5f122/google_genai-2.31.0.tar.gz", hash = "sha256:71518537de2ba967a61960cba844691dca3dd4ff07726c01a6917d132213541e", size = 745679, upload-time = "2026-10-14T23:52:24.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/89/4621aee9cb183efa5b8a187f0e315c49fddcfb8cd7f238b4a3bf4946dcd0/google_genai-2.34.0.tar.gz", hash = "sha256:8e182cf3f73384167b96861cf0aef5102b5070f2db1ea11002879e913806ef37", size = 791853, upload-time = "2026-10-29T06:43:23.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/64/1e9ab799830101ccbdcb67376a3f12b59e51f57643862999fd31af6938a1/google_genai-2.31.0-py3-none-any.whl", hash = "sha256:c87b9d495a24b09dedb29b852a201bea69ecd0b3f6c2561dc68f4055451b1b90", size = 1208245, upload-time = "2026-10-14T23:52:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c24db865d9231b1c672ea26242a4dcf340e200bb1a9823dce4aa9c4c07a4/google_genai-2.34.0-py3-none-any.whl", hash = "sha256:a4cb02aebc4c357f863d2938132f0e3f7967fc5e00b8a4078673f68aad340665", size = 1258155, upload-time = "2026-10-29T06:43:21.121Z" }, ] [[package]] @@ -2170,14 +2077,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.75.5" +version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/2b/6ce81972d5c8cab9705fddce3153be63222d9e12fd96f8baba5038a744dd/googleapis_common_protos-1.75.5.tar.gz", hash = "sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72", size = 156513, upload-time = "2026-09-29T19:26:14.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/b6/bc095b23a3e0d978e75c8c18cabe4c11f791837410670e4486f0d539bfba/googleapis_common_protos-1.76.0.tar.gz", hash = "sha256:71db0e44a1d4c2dd024ebad44ca67129cc1e2892e4861df466764749d5ef8642", size = 156250, upload-time = "2026-10-22T23:43:36.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/b9/6b29500a1c581ff4d77fd83c6568d068bee06f1b139fb6eb0a4f2d4bce8a/googleapis_common_protos-1.75.5-py3-none-any.whl", hash = "sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d", size = 307737, upload-time = "2026-09-29T19:25:48.735Z" }, + { url = "https://files.pythonhosted.org/packages/05/6d/78dc67a9e779e594b49bf62604034d3a6aa428a55afc729cafda4b7333e1/googleapis_common_protos-1.76.0-py3-none-any.whl", hash = "sha256:59e13344a9f3651aa201a03e2bee815a3bf43a4baf1f9ff8a9b62a840ce25fac", size = 307707, upload-time = "2026-10-22T23:43:10.445Z" }, ] [package.optional-dependencies] @@ -2271,18 +2178,27 @@ wheels = [ { 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]] +name = "griffelib" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/e2/0f5d8ed08766bc6dcd3eebf5a6623d83cbca33a6e4c90aef57156ce745ad/griffelib-2.4.1.tar.gz", hash = "sha256:40c028dd2e623f464423f0e11e3c553158f1cf4f1368239a370534d73e1407ff", size = 245153, upload-time = "2026-10-22T11:20:03.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/02/1156d5f5307f573b901b14373848b84e69ca6cab09169b8cc97d16a181f9/griffelib-2.4.1-py3-none-any.whl", hash = "sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce", size = 172983, upload-time = "2026-10-22T11:19:59.843Z" }, +] + [[package]] name = "grpc-google-iam-v1" -version = "0.14.5" +version = "0.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/d0/fa5bdd5f3f421bb68dc6dc162e9caaf942897ca41ce7255b524723c80f0b/grpc_google_iam_v1-0.14.5.tar.gz", hash = "sha256:07fd3a9fafb586588e771831fbfc8f6597050181d0c3b45e039d18b8fdc1aab5", size = 23736, upload-time = "2026-08-06T06:24:54.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/24/0ef24ccdd91ce07be7d6466d39becdd4fbb33e52426ba2391fe334d24bca/grpc_google_iam_v1-0.14.6.tar.gz", hash = "sha256:fc0a683ace0a149184fcd08d667dd64d51ae65e5698e86f7136aa6002a8b5373", size = 24088, upload-time = "2026-10-22T23:43:37.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ab/be3ad0d46cffe35fd1e7cc3f9947edd6cb3c552229de3be2742f15f7ea47/grpc_google_iam_v1-0.14.5-py3-none-any.whl", hash = "sha256:0f5e680b20aa0a9441e68c769da04d94d70fca4e43751a82d8abb8aa6a7181ca", size = 32674, upload-time = "2026-08-06T06:23:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/61/e0/0524fbc82835c22e711ac3e7d7dfb3ee997f99f5eaa22ba6b31cc17b753d/grpc_google_iam_v1-0.14.6-py3-none-any.whl", hash = "sha256:042705a3ce306f289b9c6eafc3453b23f953bab955adcac3c36545758ccd681a", size = 33089, upload-time = "2026-10-22T23:43:12.096Z" }, ] [[package]] @@ -2299,77 +2215,77 @@ wheels = [ [[package]] name = "grpcio" -version = "1.84.0" +version = "1.85.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, - { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, - { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, - { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, - { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, - { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, - { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, - { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, - { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, - { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, - { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, - { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, - { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, - { 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/f0/03/f9e805772d4d01da2a1754a5f63c58601eacaf8f1e878729217188de7d16/grpcio-1.85.1.tar.gz", hash = "sha256:5f530287d5de5f7f66089ead81de44f6787162944dab3922efa65dfdff108a1d", size = 14091456, upload-time = "2026-10-30T16:01:16.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/3c/ab73cb485967ac57f00350fef66c8180400c6a832436cf39811cc7d41f8d/grpcio-1.85.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:d2a776b113e7bd6d139e679f5da281193d6410cce361e4180c9da0d553e01927", size = 6823898, upload-time = "2026-10-30T15:59:12.079Z" }, + { url = "https://files.pythonhosted.org/packages/81/0f/80bd2a767a8cb78e9c257b9062f9786b67c3ce26d403825545ba2e9a79ad/grpcio-1.85.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cfa9bf4daed57dd5bd5fac4be3276f5dc1703f41abf4df0737f4e7952705cbc6", size = 12654220, upload-time = "2026-10-30T15:59:14.324Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a0/efb97c6639f59907fe3c17270904918b37cdc406eba2dbe97feacf309a86/grpcio-1.85.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9885436e83eab92dd0d476642baf08a17c55ef2b1db430adcba3e89bb6938a5d", size = 7215807, upload-time = "2026-10-30T15:59:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f2/2b0900585deed45e126a1d5939acbaba94f6f4c62a787949cdc03329979d/grpcio-1.85.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3bbf7ae798e38eccb1b7abf169d9e228514418da6f68caec69a295fde899ec17", size = 8200549, upload-time = "2026-10-30T15:59:19.657Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4f/30f66cedfec6c03bc304b28fbe073ed53179cf37a511b8ee8e8e7e2d3f17/grpcio-1.85.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83aced2a1c05648cee0de34e531351118b14c58238f0a61b43559ac8851647a8", size = 7392335, upload-time = "2026-10-30T15:59:21.917Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ad/5dee0e6e5a57f88379bc2820aa6e3098a2174e21a40bfb961c6d5f5b28d0/grpcio-1.85.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b364bc3daa601eb1cb0023e90b1309cbf6da0de6ca40a19f2d5a31edeb9990e3", size = 7955183, upload-time = "2026-10-30T15:59:24.197Z" }, + { url = "https://files.pythonhosted.org/packages/43/d8/93b729c8fc1aee6232906cffd44a9dfcb6f8387e6279a1238cd972ff468a/grpcio-1.85.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c9a4a4e504bd914e10c0d6190ffac59b9e21f1c962366b72293075ade097793d", size = 9212733, upload-time = "2026-10-30T15:59:26.388Z" }, + { url = "https://files.pythonhosted.org/packages/08/4d/d6339c51032649a90c347a67ad41b56f2f1a564f4960efcc72409fe03eba/grpcio-1.85.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83b09ca3f9602caf4f1b4295b4bb002b10477789ff9797feeedc811b9a6be0b7", size = 8332651, upload-time = "2026-10-30T15:59:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/085116e89db4f758af02eca2da9ad3a1890fb1691f5e2fc882c402ed9622/grpcio-1.85.1-cp311-cp311-win32.whl", hash = "sha256:bbc678a2b8d10e0dea666d948f36237e681be3686079564937df689bac386bc5", size = 4779959, upload-time = "2026-10-30T15:59:31.001Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/fb80401f9eaf51eb2fc8c5dbf8e4f8cbfeebc21bf837f7c6576790ce433e/grpcio-1.85.1-cp311-cp311-win_amd64.whl", hash = "sha256:27b519f6594da04cf2dda1378f46811b2b6160d60a01f88a0ec4de009bbbfa46", size = 5615553, upload-time = "2026-10-30T15:59:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/31/8c/b6d9ea0bf02a6b47ba34c5f936da2921d76f5e40cbb411e9abb246b70d2d/grpcio-1.85.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:1a10d337967ec2be049c6a2345320f6a59f71bf9f54febfd80e5a966cffaa945", size = 6797195, upload-time = "2026-10-30T15:59:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/f60cfacd679fe811944be391c899955c3d9c7e07db922b47e535ddda550c/grpcio-1.85.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:658a9d89c5736bf4aa3ef4ecbc79528e26d8e23c5e1c83417d2870a1a06063e5", size = 12633025, upload-time = "2026-10-30T15:59:37.66Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/5dbe225f0ceb208538dc8c16c5f52b0c402b91611f981604b5e280439dfb/grpcio-1.85.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:843aa281a48188621f50880496d35defed51298bcb66d5345d5f282fd3f87354", size = 7170616, upload-time = "2026-10-30T15:59:40.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/ce972a4e5bb721ba85f5c9e20a0029253ae0b2deb5276dafa3383e025133/grpcio-1.85.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e768556a2346d1d2921f511f3145e2462e4fe68095c31e4db407558bcd3f1d4f", size = 8180786, upload-time = "2026-10-30T15:59:42.7Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/74a8649d2787dae0a661855cdf09a8e2238fa741689bb368ca1b4f6d5b6f/grpcio-1.85.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1a72d19c0ab84b4b73b6c47eeaade76d84b1973a51593ff1160a73df33d03d9", size = 7362744, upload-time = "2026-10-30T15:59:45.076Z" }, + { url = "https://files.pythonhosted.org/packages/03/72/87ef13aba6bd2c5c6d4c5117238b17b1e3c1c1efa26c66cb377053124eb7/grpcio-1.85.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0e07f477e1d1a05bdbe71d63add964c5457e7ab2e3f5fd32859639ed8ecb976e", size = 7929643, upload-time = "2026-10-30T15:59:47.294Z" }, + { url = "https://files.pythonhosted.org/packages/09/f1/2e75b2906c9643e0be81a74bf23ae10c29661e0199c8f3a6e2c23707e882/grpcio-1.85.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2846c70ed58c8e149e8e5d867af40805a13b151d84157d8272952d711a77c5c", size = 9181161, upload-time = "2026-10-30T15:59:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d9/6b9127e8fa3ef14a3ec199436bec6c1a54a0bb429ca261b1a2fb518938d1/grpcio-1.85.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9998f03ac0dfed226eead6e93cee1b547d7a32e47e1915dee94eff0e113b167e", size = 8318829, upload-time = "2026-10-30T15:59:52.09Z" }, + { url = "https://files.pythonhosted.org/packages/21/46/ff3374d33efee490a0e50e3c3fc59280b7f7fc2ed3ab1ee8c4e9873dc83d/grpcio-1.85.1-cp312-cp312-win32.whl", hash = "sha256:2670c03d154f6bec66a0fef97c607116761b032183187dfdfe229b5975820a4f", size = 4768056, upload-time = "2026-10-30T15:59:54.132Z" }, + { url = "https://files.pythonhosted.org/packages/dd/48/4030a63540089d00a36cea15282623bcf084a5ea4c0ffc4685a42ef0f342/grpcio-1.85.1-cp312-cp312-win_amd64.whl", hash = "sha256:eaad7e6aa9ac3aded4f6bc86a18bf2cff3de356f972bf32a5558df9788085b85", size = 5607218, upload-time = "2026-10-30T15:59:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/c1/05/2d40be7dbfd8e66e8e5675ec695fafacc69823dcd2ef5d387ad7431c7af7/grpcio-1.85.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:e117b074d707d418ff411e0ad6fc86e97e83047b742488b41d7a5b7a62953cd3", size = 6801759, upload-time = "2026-10-30T15:59:58.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/98/530af74d704dc8e79c79b26888af44ccbdd1827bfabc9cb405b3d4cc184e/grpcio-1.85.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:6bb4bbb5cb46010687db552fd88852c405595814fb7d435b072124bd6b93f827", size = 12629341, upload-time = "2026-10-30T16:00:01.159Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/bac12e6c7ab3b1c6b50816fdcbf7f97aec1bd0992cc0d193e332aff716a5/grpcio-1.85.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bde4264ca71aaf3bc56c587380c426b1c959d1db0ab0a3f632bc66515f9726f", size = 7174387, upload-time = "2026-10-30T16:00:04.847Z" }, + { url = "https://files.pythonhosted.org/packages/90/0a/7770a652508a3a6c7d16fbb81db0f0b2be8a73a469af88a6cdfdc3d29cf2/grpcio-1.85.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7b931335c163510fb054c30007fe3556ef3bf354a1c37b5da403f2e52c6bbd95", size = 8185778, upload-time = "2026-10-30T16:00:07.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cb/52417fb272b7603d13ec8688e843dfd8df64584da1951ad954b9120d7c80/grpcio-1.85.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:de520b1555ae445cdc4f8f1cc7ccba12a19992bfafa7c292ebe7fe1fe5da6c53", size = 7364062, upload-time = "2026-10-30T16:00:09.777Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/9329799fa9defe9327084b9ca7b9a9745e83e7174bb90f2bfb2f37063967/grpcio-1.85.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2b20ba567ca818b8393fd4668878b5688cab32a46e8e7a4d1a58641fc0072df9", size = 7932775, upload-time = "2026-10-30T16:00:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/48/e4/826e0ae8f89a3d2ab3a15b91ee4ef486bdd7a09fd1ff72e12d01505341dc/grpcio-1.85.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f0ebfbd8ba42497e0ff7229c8c5e166e07c2b348462c1481dbd90faa978ad8af", size = 9173461, upload-time = "2026-10-30T16:00:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/88/2d/d4baf37bf1a9ba8222dcb133c6aeb09119b183bd7b471061762a1198461c/grpcio-1.85.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:000999ea9bba80217b1625782d638e7a688790f6154e845e18826f7da859bfc7", size = 8311104, upload-time = "2026-10-30T16:00:18.533Z" }, + { url = "https://files.pythonhosted.org/packages/e4/02/b07af232a583727fa84665d11165bbd7be6ddd31d84c2b8cb62a7b69062f/grpcio-1.85.1-cp313-cp313-win32.whl", hash = "sha256:b3722f4e01820f4d2422679a567fe33f31b6f14eba4352657a1078f4b4ed323e", size = 4769351, upload-time = "2026-10-30T16:00:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/d2448e860d884e8c89dd9135cd9373df064c10227d90973d822c2125428d/grpcio-1.85.1-cp313-cp313-win_amd64.whl", hash = "sha256:c17d61af28b322c48ab3881c685e5f6e7e367c56844cf589ee7707be9477b969", size = 5606831, upload-time = "2026-10-30T16:00:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/39/cc/da49c22b9c571b5315acc507e2ff6568e5ff9d4c99895285910283bf87e3/grpcio-1.85.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:2a378eb60ea42996be5a8c7167302fb30d968fcfcf9edd7b3fa6c985906c4500", size = 6803626, upload-time = "2026-10-30T16:00:26.587Z" }, + { url = "https://files.pythonhosted.org/packages/12/92/35bcee92bcfc2b0f66290a5dd6dd23ced7b9423fe269cf4495136e73d55b/grpcio-1.85.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:90a7e0f26de39d15f5c741b8848bb2065c9ab7351c57d4fc973ff5cbda0cb9c7", size = 12640307, upload-time = "2026-10-30T16:00:28.969Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/469b69dadb2c413706f16e0c3e7a2749cc70abaab10d718873603bec29b2/grpcio-1.85.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:421925365f9a665830eded01b8053033abbc2fbabc9bb4e9c59c3b5b12acbab9", size = 7187308, upload-time = "2026-10-30T16:00:31.652Z" }, + { url = "https://files.pythonhosted.org/packages/c4/df/2fcb1567d37e91ce26bddfca30f39fe6c39cbc5a2f4a210cfb1246ced1b2/grpcio-1.85.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8a921a98ed6142a29651b4a6f4c7b8e051d115f1e78c31655a9642dcf6356dd9", size = 8188002, upload-time = "2026-10-30T16:00:33.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b2/c67c1d8c7b70a094d404b28233458887bc1ca1319daa95d8952621216717/grpcio-1.85.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e0e6d5829b50a2ffcfcd2dbf08d02ac0d6919051a2e97c2ee27c99bc9916a245", size = 7368045, upload-time = "2026-10-30T16:00:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/80/48/5cf6c704be4b059d508e8f6c981154223533595ba7a3ee531211ef8be62e/grpcio-1.85.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:58b5ebe509da292bec5a1913c1e4c14d842a33f6cb330a87d3713c5089dd0f55", size = 7941330, upload-time = "2026-10-30T16:00:39.173Z" }, + { url = "https://files.pythonhosted.org/packages/67/96/5ef14edb784ea2053610753993601e98a76a18b541a8a74103dc390540ab/grpcio-1.85.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:016982ebd21d2b48e955aa808e9db8d628708a35c6eba852cd0e70765826ab06", size = 9176909, upload-time = "2026-10-30T16:00:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/50/01/4264ae95364bdeede4c8a5f0d83df83a21b477935f9ab8c829ae5606e1d4/grpcio-1.85.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fa3d7b8b95e8a609967991c3b0a302b2f9d510fe417efadfa7c7d2d044a99ba", size = 8316226, upload-time = "2026-10-30T16:00:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/75/b8/79dace38eb4bb904751b804cfdc71609a3840f34084af7475169059dce20/grpcio-1.85.1-cp314-cp314-win32.whl", hash = "sha256:39b1965266b579f3758db20d74f9075b130d1b1edcf0464cda316e403a15da9b", size = 4853260, upload-time = "2026-10-30T16:00:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c9/68a6bbd2450f08942c3056acd8995ac28af2b4eeb0574c0a8e59fa59b140/grpcio-1.85.1-cp314-cp314-win_amd64.whl", hash = "sha256:5f015600190124813032da0560ec950c13c257f77c1307abd77c393c0994f76f", size = 5758958, upload-time = "2026-10-30T16:00:49.145Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/1a2481b354ab4528eb8b3462937904c9206e2a54748f3f74a2576205ef3e/grpcio-1.85.1-cp315-cp315-linux_armv7l.whl", hash = "sha256:07bfef5df75875d8481d3403f864785e30096594162e8041c53a84fb11ee4f8e", size = 6803392, upload-time = "2026-10-30T16:00:51.782Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/1868c12c18058d778fcecee0e04c867891a82d983d2dd0de2639158aa960/grpcio-1.85.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e909e0f9145a555d3968dd2a448616b0d3cb58748b9e2053de3e7974b3e429bc", size = 12657793, upload-time = "2026-10-30T16:00:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/ed/af/c283032a42df5217913b98bd0113738b68dd94b91cea16b2209565fefe67/grpcio-1.85.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1f3064e209167e5fcb75a72cbc18918c06027b92b29fa06eaff0126f9a2aab5a", size = 7209146, upload-time = "2026-10-30T16:00:58.279Z" }, + { url = "https://files.pythonhosted.org/packages/45/64/8a72fd6c20c206cff07eaa49b2ca63bb15d7ed9b6bf7c653ea10ec578e4b/grpcio-1.85.1-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:07675c635a2da0c772e58ac736a7fb10c7c7c1613157766441e6866ad97d25b4", size = 7511067, upload-time = "2026-10-30T16:01:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/32afa404288c7eea4c6034d031f1a07eff772a8cdbfc7751314382e7b724/grpcio-1.85.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:759f7df78954e3ac0ed4c87612eff9125021b9271be37be8c88167aa533e29ee", size = 7410227, upload-time = "2026-10-30T16:01:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/1c51e2c2d0d9e4d37e8e8a7be412c5dfb3fd5c80ab100dd531fd81b44d43/grpcio-1.85.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:7cd08bd5ad063ad9639dbebf95d3a6b8c8aa2c9d49049bc0e8f47e73ecf1c1a2", size = 7964886, upload-time = "2026-10-30T16:01:05.208Z" }, + { url = "https://files.pythonhosted.org/packages/07/f3/e26f570129c162d8f15a02af6edb49e592ac1c88a63490cd3ec9142d8c67/grpcio-1.85.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:2198269c8b69cb1e9d5d20628029245e40b7227d5f6fc6f298a420701d8fb652", size = 8504060, upload-time = "2026-10-30T16:01:07.82Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/e334cf7442c01892ed813563b21b2a8ab5b042e34f33ad76cf64526d1de8/grpcio-1.85.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7bc5de50374416223bc35332aa8185567ec065f86190ff0ca6e13176ed39a3b4", size = 8346563, upload-time = "2026-10-30T16:01:10.394Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/cf72913ce0baae1fed95445f1ba8ac309184ac16b4f20fb5f88219eff72c/grpcio-1.85.1-cp315-cp315-win32.whl", hash = "sha256:5740aedf1e6acde538d7b80a2507eb6ee0b8983c577a36890d27a0e2b62cc1eb", size = 4853404, upload-time = "2026-10-30T16:01:12.239Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b8/b7cff7b9e1ff7b6a8d36a19ce8b9c575963e71fcbb3878b276cff6748b05/grpcio-1.85.1-cp315-cp315-win_amd64.whl", hash = "sha256:b32f39c56d22d2e8722eececd4376074d94e01595a29f4f944ae0b6cea1caa70", size = 5758584, upload-time = "2026-10-30T16:01:14.325Z" }, ] [[package]] name = "grpcio-status" -version = "1.84.0" +version = "1.85.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/16/0a161d0016f6e318ebf217780a91e007a67d9d244a06f79caeef689f5f02/grpcio_status-1.85.1.tar.gz", hash = "sha256:3f6ba6090d794b7148b23f03355addd56bee2b78174a12784c4d9b4e28c8928e", size = 13982, upload-time = "2026-10-30T16:05:25.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/5e34e2543b586ffddf90721ab242e7b662c063484ee3978bf61ca90d7579/grpcio_status-1.85.1-py3-none-any.whl", hash = "sha256:ef84cb262c7a05fd7c27e61bb70c9f63bce4484ce8b3366585a381fed738fcb0", size = 14640, upload-time = "2026-10-30T16:05:11.252Z" }, ] [[package]] @@ -2398,7 +2314,7 @@ wheels = [ [[package]] name = "hatchling" -version = "1.32.4" +version = "1.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, @@ -2407,9 +2323,9 @@ dependencies = [ { name = "tomlkit" }, { name = "trove-classifiers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz", hash = "sha256:c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07", size = 58065, upload-time = "2026-09-20T22:48:45.398Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/f1/e47b937226ae3b093bbd0132eb0b7ee5bfad12c2b5324afd10ae49946b05/hatchling-1.33.0.tar.gz", hash = "sha256:4c422bd4d554fcff5de5b73e9596f85e41e9f97f7e553d8f7595ecb31a627bef", size = 58079, upload-time = "2026-10-28T02:27:30.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl", hash = "sha256:08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c", size = 78748, upload-time = "2026-09-20T22:48:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/32/64/4afe5dd77b2939a93e8c9513f8082dbd049ecba9c1484a379a0f6ae29d6b/hatchling-1.33.0-py3-none-any.whl", hash = "sha256:415290b84f464d2884303fca8974d38e7b85579069fab02a15db0828fbb1446d", size = 78750, upload-time = "2026-10-28T02:27:29.033Z" }, ] [[package]] @@ -2436,15 +2352,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.13.1" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" }, ] [[package]] @@ -2563,7 +2479,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.13.1" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -2573,9 +2489,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" }, ] [[package]] @@ -2628,15 +2544,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] -[[package]] -name = "imagesize" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, -] - [[package]] name = "importlib-resources" version = "7.1.0" @@ -2696,14 +2603,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.7.5" +version = "1.7.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/94/80fea1514b7c6d7d37804d3fe9ca81455f633347fc98731bd71ffe1faa17/joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea", size = 234990, upload-time = "2026-08-29T13:05:42.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/15/48ff246732232d35ee234228def22b52455fcc55577f06146602399a29b0/joserfc-1.7.7.tar.gz", hash = "sha256:471d0fd12a5a0d1c0942de280f39efe6f597878315033270b504d4d006d25510", size = 237446, upload-time = "2026-10-19T00:30:29.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/c5/82addfd375e5ee6520644e0553e4aadde92d668c4fc99cc716d337fe7bb3/joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159", size = 71269, upload-time = "2026-08-29T13:05:40.718Z" }, + { url = "https://files.pythonhosted.org/packages/df/9f/934db284aa22f37af59708e382903860d4b81329c137b1b0ed2745daeb65/joserfc-1.7.7-py3-none-any.whl", hash = "sha256:06979cfbab78bc802f14c0358f32969826cc26720e13cb38926ffdc86bc0cb2c", size = 71758, upload-time = "2026-10-19T00:30:28.498Z" }, ] [[package]] @@ -2882,6 +2789,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/24/8d99982f0aa9c1cd82073c6232b54a0dbe6797c7d63c0583a6c68ee3ddf2/litestar_htmx-0.5.0-py3-none-any.whl", hash = "sha256:92833aa47e0d0e868d2a7dbfab75261f124f4b83d4f9ad12b57b9a68f86c50e6", size = 9970, upload-time = "2025-06-11T21:19:44.465Z" }, ] +[[package]] +name = "markdown" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/32/0b4d0297064244f04af4331b65de16237cc3cce07474171ee14222a28981/markdown-3.11.2.tar.gz", hash = "sha256:87bf5d90f2d2882e2c02fcda182c2800456b985bff38913867c49706481514b0", size = 491445, upload-time = "2026-10-19T19:41:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/66/bcab3b677b71ad286b4e607e3427499537c76e13ad2e22584ba7a7d9882c/markdown-3.11.2-py3-none-any.whl", hash = "sha256:f80390747a2a0ff2ba54da074ce4f862f72a6685f608cd2637b646a6d83993f5", size = 114266, upload-time = "2026-10-19T19:41:41.244Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -3023,24 +2939,104 @@ wheels = [ ] [[package]] -name = "mdit-py-plugins" -version = "0.6.1" +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] -name = "mdurl" -version = "0.1.2" +name = "mkdocs-autorefs" +version = "1.4.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/e0/1e8ed4e8dabdcba8cba5dbf4b8ce49bb3b51e750177155247abb5345d48a/mkdocs_autorefs-1.4.5.tar.gz", hash = "sha256:856f75b327b7717923ce06267094650689a510a748c48657f5333965950c2788", size = 59736, upload-time = "2026-10-22T15:03:55.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cf/3dfc07ec910553bfb1e5c9009c1854bd9d2efa7f3dda03f007a77db0e63a/mkdocs_autorefs-1.4.5-py3-none-any.whl", hash = "sha256:68ee484e0800f3ba50cda1bc4a94293adbdfcc010243c322b3f5d66e15d06770", size = 29633, upload-time = "2026-10-22T15:03:54.091Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/cc/e3ed8b16851f5d664dffcf8f55fc60d0d3f736a3edca21d1508105d1255a/mkdocstrings-1.0.8.tar.gz", hash = "sha256:b50fc326230906425c82ee9ec59b80e5db00892b2b593d4d0b841a0d4acf537b", size = 107444, upload-time = "2026-10-19T15:43:02.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/5e/4df7a2290e97c60a3ba410084966e5ab84aa89d15c8c98c195acbaefa8f4/mkdocstrings-1.0.8-py3-none-any.whl", hash = "sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c", size = 42209, upload-time = "2026-10-19T15:43:00.65Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/43/4783d5606de4f5e4bea10388c8741b0c40d03e7cb9527b94ad35966351f5/mkdocstrings_python-2.0.10.tar.gz", hash = "sha256:5e34ae56b2f20a57807ee479d603ddc5ec7cc6d2252c4c177754627ef2d3b833", size = 207682, upload-time = "2026-10-19T15:58:07.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/1b/fc924a97a237adda47f15c1b0cc271639e6b181ab3f95500fbf060c79a69/mkdocstrings_python-2.0.10-py3-none-any.whl", hash = "sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062", size = 105538, upload-time = "2026-10-19T15:58:06.287Z" }, ] [[package]] @@ -3543,7 +3539,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.4.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -3552,53 +3548,53 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, - { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, - { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, - { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, - { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, - { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, - { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, - { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, - { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, - { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, - { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, - { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, - { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, - { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, - { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, - { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, - { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, - { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, - { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/4a/3c/78ecece71ee4b5445389a4ccd11627cfb9c69e4de554d11060171f376305/mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623", size = 4066728, upload-time = "2026-10-28T19:23:13.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8b/2bc7f489528db504221ad7eff6b3f83979ee57d066103c72a85d09046a30/mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502", size = 14321495, upload-time = "2026-10-28T19:21:16.612Z" }, + { url = "https://files.pythonhosted.org/packages/18/9a/b2f037b4982dce33645296e4394b031ade60f697f468761fa3ea8e2f8ece/mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c", size = 14467468, upload-time = "2026-10-28T19:22:10.629Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/379f9bfa18bf3f8d43668fbd9ae4f2922dd027a559c461ca9d41cde1d32e/mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5", size = 15421496, upload-time = "2026-10-28T19:22:48.005Z" }, + { url = "https://files.pythonhosted.org/packages/83/50/3bde18a2824ec296432cebe01752ccd0ab8a2ad784abe4390040d019b3bf/mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392", size = 15670425, upload-time = "2026-10-28T19:21:57.026Z" }, + { url = "https://files.pythonhosted.org/packages/6a/36/8b722b88c325d317354de7d10d8fb9d4dc0f958433e873a91fc4ed21a06e/mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d", size = 11300972, upload-time = "2026-10-28T19:21:48.724Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d6/fb2bab1245658d68af8abfe776362a780c621fe0f364f2c7b53039a945ee/mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e", size = 10873349, upload-time = "2026-10-28T19:21:32.104Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1a/0be9dc6af5e0e04128f18a5e36b4ca44bfb0085857d37aa6a7de1ac2b0cb/mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf", size = 14381321, upload-time = "2026-10-28T19:21:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a6/7907e36711a8a8c703f961f076fab61793383694140bd1fd87589f3f68d4/mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750", size = 14466373, upload-time = "2026-10-28T19:22:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/7313212b29de570c25c9859d7c8e4ef2251a6d34338a1e20031d94297721/mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494", size = 15553666, upload-time = "2026-10-28T19:22:36.511Z" }, + { url = "https://files.pythonhosted.org/packages/23/8f/92966b6082ff49da9142e1e527eb9af4359a9a61632a1cfc313da96b37fe/mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531", size = 15802641, upload-time = "2026-10-28T19:21:13.421Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b4/874f6fd54b005020ad63e2cf7a59b178d16a771d478849b29b842cb0cf14/mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5", size = 11383067, upload-time = "2026-10-28T19:22:03.516Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/5ae8e6528c2faa96e4a921a999d2c7ac7c565bee184b4282bf62ffa76b5d/mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be", size = 10946882, upload-time = "2026-10-28T19:21:39.51Z" }, + { url = "https://files.pythonhosted.org/packages/6a/65/cd19bc06b67d2df4828220245fd98569ba40fac00c60ba48bef1817d8ac3/mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45", size = 14214179, upload-time = "2026-10-28T19:22:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/4d54ca4f49a0113d0c956980272aefdc6eec5458f7a72b350785b9ef2249/mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b", size = 14376217, upload-time = "2026-10-28T19:21:08.019Z" }, + { url = "https://files.pythonhosted.org/packages/a2/45/089b4724b85f0f408941be5298f817ee89181c1ce031a73d60ebb150c2c7/mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7", size = 15467623, upload-time = "2026-10-28T19:21:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/15edbc64b0b1412bfe94c74192b3b56554ab642b85a31bb06cef6313101f/mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296", size = 15729867, upload-time = "2026-10-28T19:21:37.557Z" }, + { url = "https://files.pythonhosted.org/packages/76/5b/7d160fb685994b65b0ca86d352f3afb5424f96a9e6914719f7b861fb6701/mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56", size = 11390802, upload-time = "2026-10-28T19:22:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ea/453d27f8d531f57c5f14aa73094eaa48e3fb77f680bdede2045cb64fa58b/mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745", size = 10950427, upload-time = "2026-10-28T19:22:59.003Z" }, + { url = "https://files.pythonhosted.org/packages/7d/75/282d43d6fea7be984ac63e75ffd8eee146ba2f3b077bb1ca5bf3f973e0c4/mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456", size = 14233251, upload-time = "2026-10-28T19:22:44.977Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a8/9021263cc7e761451c7d8131bdfab3c0a5c2d67aa2d694670019e1ab86a7/mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c", size = 14394953, upload-time = "2026-10-28T19:21:29.492Z" }, + { url = "https://files.pythonhosted.org/packages/16/10/4192a5cea09ebbc41c534f57bd0d8f0fb42a8e810632cda6e37f072c59fd/mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453", size = 15442824, upload-time = "2026-10-28T19:22:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d3b0a7474aa4f696594a47a218b6796d9c4346fc81ed288904278cddd9bd/mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a", size = 15717227, upload-time = "2026-10-28T19:22:54.026Z" }, + { url = "https://files.pythonhosted.org/packages/48/6a/2ec530dacd4f77ca50d0fa05b854098f2d75e5d18c587d2612b44f10d68c/mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a", size = 7898637, upload-time = "2026-10-28T19:21:41.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/de9d7bad12e3868d5f1f4279aed20b1b568b2c5e166f2d3a05b211dca0b1/mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a", size = 11576656, upload-time = "2026-10-28T19:22:13.232Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/4fa9883b9b59e5c96ff62845ad019dc47786031c45d67eaccf452cffffd8/mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6", size = 11174403, upload-time = "2026-10-28T19:23:01.346Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f2/827e356fc4f74c60b8da42663336e035b49d948a3c11224b0fc7543086b1/mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93", size = 15713427, upload-time = "2026-10-28T19:21:26.757Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/e58a4fcae1d238189eac758036c0921992a86e3e6209190efd96d61fc5d4/mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276", size = 15938481, upload-time = "2026-10-28T19:22:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/54/a20f881801b88900923f2be1541a4b438ad0f0883e03836641fdc3d7adf4/mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373", size = 17243006, upload-time = "2026-10-28T19:22:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/de/1c/ff84a6133a6e89c390bff6b25d9bad6598e7891b205f4b30b496906d636d/mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6", size = 17557728, upload-time = "2026-10-28T19:21:43.607Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/effa47d39b8fadf679c08e7c78559e523f9769a42a75662d2d15b2b9cdd6/mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca", size = 12370826, upload-time = "2026-10-28T19:22:29.779Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/b214ececb1a97fbb61ab3dfd48ae3cffd92ae2c148af4c9dc94c6c56772b/mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971", size = 11851083, upload-time = "2026-10-28T19:23:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/57/5b/e44a2edb0b25a9f1429de770e1d0238af23302671dd40c77ac5efeb0c559/mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c", size = 14243144, upload-time = "2026-10-28T19:21:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/d4/5c99dc3a28e11d2f0fcbb02df63d2d3e574dedc0f9f69d50901df305a7a6/mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7", size = 14409316, upload-time = "2026-10-28T19:22:15.515Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c9/3108b38f4c30eabd80194f5d9771a87e27196c5e0ddb5bf4b99c6ed1e97e/mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2", size = 15445573, upload-time = "2026-10-28T19:22:05.635Z" }, + { url = "https://files.pythonhosted.org/packages/dd/12/84a53eceaf11c46dfd7931fb6b8902d01fb33787d3a991e81c461458150b/mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb", size = 15732989, upload-time = "2026-10-28T19:23:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/65/ba/bf017dc2abdf83b2f74e29c43834b7af67dc1f1b20202f02dae60b4781a0/mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692", size = 7898932, upload-time = "2026-10-28T19:21:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/76/05/0c22b1f0390120a8592d31209cb6b9f932b6a0b426fe643e550de15139e3/mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100", size = 11577577, upload-time = "2026-10-28T19:22:01.302Z" }, + { url = "https://files.pythonhosted.org/packages/73/fb/ac01e173629697ddac2f11e51efd10347d673f9a7311b03f4aba710ebccd/mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704", size = 11173540, upload-time = "2026-10-28T19:21:46.444Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/b9b9c8eb09e70927ee65b1456cfed35710b6e4ff0b07f364ceaf2202307c/mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1", size = 15706623, upload-time = "2026-10-28T19:22:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/26/74/1ceb1ca884add0565e618d013dd24333039100dc0fcc42661561f2e1cae5/mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca", size = 15944095, upload-time = "2026-10-28T19:21:04.779Z" }, + { url = "https://files.pythonhosted.org/packages/2b/68/27bcf9b4efa22733b6050485ae37845b94c9b8851abe4fc60632d905c32d/mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1", size = 17223065, upload-time = "2026-10-28T19:23:03.724Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/ed5724b317bf585c29f3c8be2792e97d45925d65a9c8af40ceeb267072e6/mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9", size = 17560589, upload-time = "2026-10-28T19:21:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/005185e6ab4752fd6cc2d1a6d1421fe21979770afe2490c00c555f34a913/mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1", size = 12353324, upload-time = "2026-10-28T19:22:51.481Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d6/20420cc9c780087139662534d128c2f3274361f8bb7e9cf0d5495a846ed5/mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f", size = 11845599, upload-time = "2026-10-28T19:22:23.136Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/6e8010f8f2c4ad817364592d4b3883633417269b913fc9d5174fdeb7e565/mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999", size = 2801286, upload-time = "2026-10-28T19:21:19.516Z" }, ] [[package]] @@ -3639,30 +3635,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/dd/b3250826c29cee7816de4409a2fe5e469a68b9a89f6bfaa5eed74f05532c/mysql_connector_python-9.6.0-py2.py3-none-any.whl", hash = "sha256:44b0fb57207ebc6ae05b5b21b7968a9ed33b29187fe87b38951bad2a334d75d5", size = 480527, upload-time = "2026-02-10T12:04:36.176Z" }, ] -[[package]] -name = "myst-parser" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, -] - [[package]] name = "nodeenv" -version = "1.11.0" +version = "1.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/27/79acfd3d6f526a4a16f3d209dedcf7b5bc1ea15c5bf2bbb82f2c2285057e/nodeenv-1.11.1.tar.gz", hash = "sha256:d7fa027c1df437bdd974b0502a8dc76fa1b3183e369ac9539602bc6bab9100ba", size = 99853, upload-time = "2026-10-29T20:54:04.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/40/35/a09e0b1dac4027e35b872e4f123a9f9e7bbc39e6424bdc9776920d3a6491/nodeenv-1.11.1-py2.py3-none-any.whl", hash = "sha256:bc0fb085d8ed0d67a6471892535f3d9b795eb911b7cd129d3a24967aab7a538f", size = 34473, upload-time = "2026-10-29T20:54:02.752Z" }, ] [[package]] @@ -3836,18 +3815,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/7f/c2d1b436b6e7cfebac140c2579a298344b85f2991a2ce5c3615cefb29400/numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f", size = 10883718, upload-time = "2026-10-10T20:05:28.547Z" }, ] -[[package]] -name = "numpydoc" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/2f/5e9bf8ed0a93587b8089242b4926dfd04d4b3b0b65f04345d219a1225f03/numpydoc-1.11.0.tar.gz", hash = "sha256:26db0adaac8087e98da05c86b11198defd962aef7c21f4dffd2703c8c6eb4187", size = 101575, upload-time = "2026-09-15T16:31:08.905Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e5/f5c38d4899cd8edd5b522071c5658202077d113c3225484d779cf257970e/numpydoc-1.11.0-py3-none-any.whl", hash = "sha256:493ad2ce0d8275ca5da484ec37dbdbbc1334c3a6968dffd10ec729b51753e15e", size = 71464, upload-time = "2026-09-15T16:31:07.521Z" }, -] - [[package]] name = "oauthlib" version = "4.0.0" @@ -3859,80 +3826,80 @@ wheels = [ [[package]] name = "obstore" -version = "0.12.0" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/65/e14b7dba4ed71c3042fb9605fbfe3a8681bade1f5f90f18adbdc635b7c65/obstore-0.12.0.tar.gz", hash = "sha256:3f355f3333084172a8e8da2f62f929e2dd5d0909f6ba714a0b42cb1774a5be3c", size = 138191, upload-time = "2026-10-15T16:41:38.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/18/8c236dc6c93c57f3796afaa3a11c7ce943da2cf268bb97cfe65ebd6db9e8/obstore-0.12.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6fddc673fee8ddade9d49bff32de4815afd30b28ba25cbdc29d87639149cf62a", size = 5417987, upload-time = "2026-10-15T16:39:52.308Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a7/f8da19362e555e1b826509c6197ba2f98f8a700c4fa4dc0761c2401453f5/obstore-0.12.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:656b9ae76280951f54503e883acb693f0fb4a3778d559733bdc4b996b2078715", size = 4615162, upload-time = "2026-10-15T16:39:54.81Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/e342f399a8c664a6f3ad61fb1a4a1f77e935d14bc7aaba3525b1a9bcc93d/obstore-0.12.0-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b942f2cadbdd35d1a37df8c56a51a00e8279feafb9b3fde4ab161b5ca7a114", size = 5025558, upload-time = "2026-10-15T16:39:56.61Z" }, - { url = "https://files.pythonhosted.org/packages/14/0c/4612546a8701c9d951920e0087be25d74865faed852731de69b2d9d63f51/obstore-0.12.0-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d87a362bfb078e015c2ab588ac38274db917c5563be2cd0306b51347db7d6edf", size = 5268378, upload-time = "2026-10-15T16:39:58.287Z" }, - { url = "https://files.pythonhosted.org/packages/fe/90/059ea17c30eef9f1025cd171d47360ea701ba8ca619302a75cf2a176bfdc/obstore-0.12.0-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8d00b374ee368db3e8f537b4bab7c45bf6077d3f6c07ec93281f1cfd087271b", size = 5438991, upload-time = "2026-10-15T16:40:00.439Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/fe36b87c170b7bafd6ec6162f923eed213a503e621bc699ee2fc296a0e93/obstore-0.12.0-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7789fda63972d2fac0107afe5aac8d362b4d248971217f26a29bb15ac21cbdf5", size = 5348697, upload-time = "2026-10-15T16:40:02.618Z" }, - { url = "https://files.pythonhosted.org/packages/70/3f/57e4173cbd61f1354d4f3f289a61a4271d8b3afa3a7f2efe519014697043/obstore-0.12.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa53318c8892c612a58d0ac6c1cf26d9f5b697ca880de4133e9bd4868e90e5be", size = 5575588, upload-time = "2026-10-15T16:40:04.29Z" }, - { url = "https://files.pythonhosted.org/packages/89/cb/ec80f278d834330b40834e34cf1332444fcdebe21c43f67e34b323ca2ece/obstore-0.12.0-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:bf6b48324a6fec42088a9918c63ab064567d9544414aac086f65adc6c16ad2e1", size = 5340287, upload-time = "2026-10-15T16:40:06.166Z" }, - { url = "https://files.pythonhosted.org/packages/e4/01/bc007efe8a55ba4131a331fd47858d188ecaa3e23cf59bccb33bac2bd1c8/obstore-0.12.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9980becd715aa1cd7bd7ca13eeabae176e6713b6e39577622d964e31540a65e1", size = 5547826, upload-time = "2026-10-15T16:40:08.019Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ae/57793c16df57064f062747d35a31167add5b68a7bfa1cf6f8ed929701b31/obstore-0.12.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4b5fb3ef86cba98b280c6f02021274060787d5984ffe3791fdea9cdcea3f4bb6", size = 5249557, upload-time = "2026-10-15T16:40:10.148Z" }, - { url = "https://files.pythonhosted.org/packages/e5/37/878d4cc92c55114ee513bb77d4194ccbb99a87920e6345a1e972057383ec/obstore-0.12.0-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:ac553dbe800be6b64cac1a4725c620ff5d474fd543deca90082dbd1fbfad2f1d", size = 5393052, upload-time = "2026-10-15T16:40:12.118Z" }, - { url = "https://files.pythonhosted.org/packages/4a/6d/a9e560f1232446cf1fc3cc1f03b5b7a9786e8312b6e543348d75a879e608/obstore-0.12.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2956f243e459413f1f4bbd1195c721ca5a6c16d7de8bcd8241c2545e0bb87c18", size = 5809599, upload-time = "2026-10-15T16:40:14.398Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1a/7586a93446b8ffff7bf9a1b41b1977ab125f4ef45e96d9b7b3f468961ffa/obstore-0.12.0-cp311-abi3-win_amd64.whl", hash = "sha256:7aef72fbe4631dd5462714309e55a3ceacfe15c5f31f518ddac41cc33a8cfcb0", size = 5341295, upload-time = "2026-10-15T16:40:16.498Z" }, - { url = "https://files.pythonhosted.org/packages/aa/97/8e68f15b93529939c6390bbbf59114e11017580629b8d14c4046dadb60d6/obstore-0.12.0-cp311-abi3-win_arm64.whl", hash = "sha256:e64494202759211cc42fa381d8959982473c9080a21aefcbf20121bdfb49a2cc", size = 5212128, upload-time = "2026-10-15T16:40:18.659Z" }, - { url = "https://files.pythonhosted.org/packages/4b/da/e51e62cb95c6c5531e49ea3aaf15df0e9fb34be0101bf4596d55afa6f820/obstore-0.12.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:728347d647edf981b7129899981a73ff0f0467225896d3747a6d4e993b7ee441", size = 5434598, upload-time = "2026-10-15T16:40:20.519Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ba/1c2b8afedea56950785e43bcb0efb7e06e449ca147845e267e2e84fc317b/obstore-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2982abef7caa064dd412236d41e630a9b49dfb8265f62ca004059446624e9a73", size = 4584517, upload-time = "2026-10-15T16:40:22.326Z" }, - { url = "https://files.pythonhosted.org/packages/29/10/9607706283e00a4f3a8370a3d77486fd72e53e089734720054755846bf29/obstore-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa8e979499741c57798d2daa048e4f42835ac9a8497f9255796db478e9a38d1b", size = 5419035, upload-time = "2026-10-15T16:40:24.283Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bf/57f52ca4a48bdb469a84cf4f3334e60324a5dfe24f7bfb180e3a7b1a57ca/obstore-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a3da66f68a98c559011f0099f9f3961db27df84218576cc1f95f4a88b5b8a9", size = 5346814, upload-time = "2026-10-15T16:40:27.197Z" }, - { url = "https://files.pythonhosted.org/packages/26/ba/ecfc87519c17b2bba6c7275d754631b6ed1232f3f35e156b7a6749ba37cf/obstore-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c1cd6498e52410f9663134c277b07234960ef2a78eb4988327c2d5d9345986", size = 5558323, upload-time = "2026-10-15T16:40:29.812Z" }, - { url = "https://files.pythonhosted.org/packages/56/49/4943918d58eff01789420f399484f867cef172822a7b77ec6087a3d4f6ab/obstore-0.12.0-cp314-cp314t-manylinux_2_24_aarch64.whl", hash = "sha256:a30dd09e62cb59f4c181f69b4e76780d6eaf0b3abb771fee3ac03f0e63109d1f", size = 5326762, upload-time = "2026-10-15T16:40:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/dd/86/1eee013a94be33eea2ea618af4f3777b620dea7112e0c1b5356f082622b5/obstore-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2622dc12100ca4b870a987ab9c0f4c3c1ca7122f14459bf66f834774790c9673", size = 5537435, upload-time = "2026-10-15T16:40:34.244Z" }, - { url = "https://files.pythonhosted.org/packages/45/b2/df7567ce2e689999f8ec455a4f825b83e6c4c3935213696a80050f4fe402/obstore-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f89ba827fe466d31cf7977525ce9ce7072e1395c98920b4846ae84a24c0b21e0", size = 5787179, upload-time = "2026-10-15T16:40:36.886Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/f0ccf26d95193c7f0e75183b52926a4b30f5af69b1539f6f44b27520a43b/obstore-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:75dabd1c785cee1d1cdb7fed4906eebcaf7dbb3ed7faaa862b6f54af59444a22", size = 5319434, upload-time = "2026-10-15T16:40:38.857Z" }, - { url = "https://files.pythonhosted.org/packages/13/54/f80372e4773977c8f83f74ecd234c53273db32bb2f173aa7c49d5dbc805e/obstore-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:09f6f78e888a4b5a037b2c405456868102e3478a3fedcb3096afc51c26028f67", size = 5190455, upload-time = "2026-10-15T16:40:40.781Z" }, - { url = "https://files.pythonhosted.org/packages/ab/39/b80c381a82819f226219a588abdaed8bd30f27deb3afea99845a32c357f8/obstore-0.12.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:0b8c8932d5234ec718e31ddcf9c4776c0cd8c694ea54baf555bffbbb8ca297e9", size = 5481244, upload-time = "2026-10-15T16:40:42.569Z" }, - { url = "https://files.pythonhosted.org/packages/89/4a/b07c3964c48e39bc8dc704254b47c850b75c22ba3ab4a392f77903ab6503/obstore-0.12.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:d35bf87c228466d3920e7f98ca1ba1471ca864c5701f3be38aa442eb51ed290f", size = 4628787, upload-time = "2026-10-15T16:40:44.506Z" }, - { url = "https://files.pythonhosted.org/packages/64/86/e190c535ed9d951dbbf28d716f1dc26a82f04ba597b59043f993b51a0ab2/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a4c300fa1ab96f074df959e70cbdb37c0bb17d81d0b296b0d7fdd7a6fca4307", size = 5053980, upload-time = "2026-10-15T16:40:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e3/5ea350a22070352859a9ccbc007061ffb9c3393c1c37de9cdbccbdc46eb9/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac9909a21eea8ffb25dab9dad6491bac6b3234f93164cfa4b8638efb0fe248e6", size = 5282271, upload-time = "2026-10-15T16:40:48.615Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8c/c3c87eeff228dea2dba146c20c0d94c3cd4a3c2d29027ed755bc2e8e1cea/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:000677a8bd10f2752cfea37a5f5f2a81414c0df07e9c5e3af4f1d1396e90250c", size = 5462575, upload-time = "2026-10-15T16:40:50.695Z" }, - { url = "https://files.pythonhosted.org/packages/2d/73/ffbda3894e7ee0ff42ae7e58e2c6007ee0433df261f62db31fabca7186dc/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3cf58ff29e0bffacfde8dd1858b57cc38f6a23e49f9357eb3c2ace9d741b3bbc", size = 5388817, upload-time = "2026-10-15T16:40:53.045Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ce/cb0660726e2600d8fde9997cf5ddd90773379f5dd287a9411b83a7a6a29a/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38acd564329c8732ea61a4c1f4dcb1ce34c4394371c42f4e075ee104268ddb7c", size = 5603974, upload-time = "2026-10-15T16:40:55.547Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fa/6471d396c57d67ee09343ddc64a6723aa9bbe95039409981858ad708abea/obstore-0.12.0-cp315-abi3.abi3t-manylinux_2_24_aarch64.whl", hash = "sha256:c0c7d5d9735bbb06e0f824d2af64e0335ff63a18fac8cbd2f83943814779a2de", size = 5369797, upload-time = "2026-10-15T16:40:57.788Z" }, - { url = "https://files.pythonhosted.org/packages/12/81/0db46c02ffaf3a2db0a8db5a593e1d3a9c29bf7192bbb75fe6deaf79a7d8/obstore-0.12.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:cfdd2c7d04c9f81d3e110ba1e6690eddbeb9f9dfb932076624fad9076a970583", size = 5579688, upload-time = "2026-10-15T16:40:59.786Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ef/8e606cf18bc8c9c0ce2523f27b9cc7888b329bd8a5bdd6ee5cd8d7a880bf/obstore-0.12.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:0fa81ce46e1d346c79e1a183a676d4511523079789c79edcf281b394c554303d", size = 5278268, upload-time = "2026-10-15T16:41:01.68Z" }, - { url = "https://files.pythonhosted.org/packages/e8/46/c967dac44c7c9cd2a07c75d32902b04a66422ff518002700aba5b8239198/obstore-0.12.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:bb944a435dff68147e462950119e9ab92392150cad91749c668cd10fc2bde919", size = 5402119, upload-time = "2026-10-15T16:41:03.497Z" }, - { url = "https://files.pythonhosted.org/packages/4e/53/c2261822b995c8dbc71f65e4a9914f6c63c1f15befb785d6d9b77037d13d/obstore-0.12.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:74bc425a675eeafc0ce1db29f38807126253ed377c0518569e61132341f7da7a", size = 5839610, upload-time = "2026-10-15T16:41:06Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/116c124a0fad9104f83f854296483c759ead70ba2229f1df2a445cf92a4d/obstore-0.12.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:690018140a0ca0e4346ec5afc2c20608555f5f5e447d0bd5ca8b99b6cb1ec198", size = 5362445, upload-time = "2026-10-15T16:41:08.488Z" }, - { url = "https://files.pythonhosted.org/packages/04/88/20151b32d7a0d1e15fd8f0b9a9c7439f67946f68071a2f63306b1e7fe946/obstore-0.12.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:3037ae42e0f036257a442d16ac49948a3759df22571213026d1c32d6045302dd", size = 5237094, upload-time = "2026-10-15T16:41:10.49Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ff/3e417abec0bdfae2fe64b384b6cabe6417f33328fe239d155069f9924cdd/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:098c8e2a584dc991e33f3047143e7bb5c2bab1d3efc20f076fd1fdcda4fc92a5", size = 5020729, upload-time = "2026-10-15T16:41:12.931Z" }, - { url = "https://files.pythonhosted.org/packages/6a/60/0348ed2f2a00d1c7f380dfebdee049641f457e472a4afa328886db59c9ec/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d9cb87386e464a92c69ef6f3168ee2dbe21dbc0d738c7cda646e79de1e16a80", size = 5264660, upload-time = "2026-10-15T16:41:15.531Z" }, - { url = "https://files.pythonhosted.org/packages/d2/09/f724a30c06211dcb1e899149e095d00179a3cf6cff8de232735d941a823a/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfbe400a02eea67104f5ae8fcad52fefe9571c042ee1257ce2059f0be1e47eaf", size = 5435452, upload-time = "2026-10-15T16:41:17.566Z" }, - { url = "https://files.pythonhosted.org/packages/16/e9/3563a37041b4186863b98343c53801b944d3d9a1ff8e8bca6f9e3ad3f7c9/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1642e69f80e95c0a2d40d87239228145d2a99cb7720a19343301ce1df0a963d0", size = 5345777, upload-time = "2026-10-15T16:41:19.983Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ee/ff4f8179d03b59f952c26247b80e89f9cc660573dede6f38c7fc1a370fa3/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25743269e0f2b49b2bdc4068a912fdd3fc811acdff79dd886429686364c12a27", size = 5570602, upload-time = "2026-10-15T16:41:22.024Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7a/c0da11a5a8f0f6a4413747718dcaf5adc9d15da1375eaacc3eace5fdd31d/obstore-0.12.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:e6612a88cdde54db1aeea432fa68477edcc43423baa8c46fd19109fa4d8fea1e", size = 5338889, upload-time = "2026-10-15T16:41:24.231Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/ed40d03a77bfcaf3d65f260ea397fe1d50fe05e22fd560f944c15986c85b/obstore-0.12.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3171dc1e0baac61725f7722219a92082ac65002af3be6dca0bda09f9049de624", size = 5547960, upload-time = "2026-10-15T16:41:26.279Z" }, - { url = "https://files.pythonhosted.org/packages/c9/62/9f316c412975f9a43f4ba8141fe53b72a35561e6825c7d3c282255153d2c/obstore-0.12.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:777678604e9658f7edfe7cbf0c0316953cc1511dd957a6f0a513866c4161ce43", size = 5246652, upload-time = "2026-10-15T16:41:28.582Z" }, - { url = "https://files.pythonhosted.org/packages/dc/3e/27ffabf8b3d7a9115f99b7374bccdcb5b5ae106897c3e5733e9c88ce174c/obstore-0.12.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:08e9009e4996d5a57e924324fbb7fb3d5d9a41dc1645e82d87268098f40c5bd9", size = 5389579, upload-time = "2026-10-15T16:41:30.615Z" }, - { url = "https://files.pythonhosted.org/packages/ba/5e/390c745f7e7e5db8db3e6cc9344b8abaf25baf14925469cf34e3b76fb02e/obstore-0.12.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c45fa5a7200c2c1ef72cd195ce8c621de9d3cc819880c120b80610da2598d0aa", size = 5799158, upload-time = "2026-10-15T16:41:32.585Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/ac2409626548e681daa1d651bf70b79f5af49c7ab4ae5c0ea252809ef4d1/obstore-0.12.0-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:3feb6b4cb17f8e0d4569730835c3628f0a5818ea63b42fbd04b7aec98549bd50", size = 5415927, upload-time = "2026-10-15T16:41:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b3/88b609eeeacfec32ade070f2782570230a58a7f844cc1e3180e9aea39326/obstore-0.12.0-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:91d0635f8e714b3a3fccb3d4cc96e519decdd1eab4728876bcc6243e378e8363", size = 4613967, upload-time = "2026-10-15T16:41:36.827Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/76/c5/9fbb70a0914f4cd5ee7f0fd3e39e0812c29cdb4b4a8120ac7870f379e8a0/obstore-0.12.1.tar.gz", hash = "sha256:80549c9ea5478fd5d5bd75a3c9178841c7108a76d8139db05407a25a55a6a476", size = 138207, upload-time = "2026-10-27T22:51:02.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ed/9667a1b1c6022b212460210929cc2b89bd72883c1182c9ea513a228158ad/obstore-0.12.1-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8c97afecce59224b82fcb4588a3d6fbef479bd6cc2616b8f4d3d3ddba43fa6b7", size = 5431853, upload-time = "2026-10-27T22:49:38.938Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bf/78437ef20196862b6f77f112184ca6c14783ddb98eb601fea88ee07c7acb/obstore-0.12.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:60af5d92da9332014bba8d34f985b9dc692e73d1141d1775dd3a328db476f219", size = 4592428, upload-time = "2026-10-27T22:49:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/f5/30/80f63a19ace4d55ee94b7572dacf5435aeaa835e32550277c9b630fa6974/obstore-0.12.1-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf96279d243efd65d005c992e20476b3f343a54f71416d04af5d29d9998e739b", size = 5029909, upload-time = "2026-10-27T22:49:42.497Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/4943fd0e5f89db060511a7e96ac887502f8d682b044e70b8e96f7f5bbf41/obstore-0.12.1-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b22ca0d66af4c3dd130ed492f966eb7d775529ea129bf379d002dd2c4093971", size = 5276990, upload-time = "2026-10-27T22:49:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/39/85/374b2f8356f052c058be845dbd4e5930d952636bb54dde67e3219ad52689/obstore-0.12.1-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ad04de828d3726da251b95ac7cf5bf4397c026243bb88b2d087e4ca1d7506c", size = 5445620, upload-time = "2026-10-27T22:49:45.634Z" }, + { url = "https://files.pythonhosted.org/packages/df/b7/1b8c950cc7cdadb65eace8d8c7a3e53b45341b726a28a3f93d2e2db98e75/obstore-0.12.1-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:437831889a617deb632b54be8e978b59db5c34e57f7329dd5ced1f2ee2895473", size = 5355053, upload-time = "2026-10-27T22:49:47.206Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/dc6eb84ef8d1f96af626c83b3da436b1a4d6b89aa52b560d4451f9ca8ef8/obstore-0.12.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d31b255aec88d37636d374a651f5efc684c89716ab64dea6ac89d75801933af2", size = 5591012, upload-time = "2026-10-27T22:49:48.912Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/f327b606c3aa1c7cc78f7595da7df96d8751e69912afa6331b48234098b6/obstore-0.12.1-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d7ee9cde74dbe11a4630c90d7245543993f3cebc38a70aceaedcd3b3d6192807", size = 5376219, upload-time = "2026-10-27T22:49:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/22/f2/54890109d9c1c8ed16d3ad8b17f53953e04006d6b1bf013e13c56c7ef979/obstore-0.12.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4240e5cb60d091e4a612499630eebede5164c821196322bc8a5ecb4861dc7b1f", size = 5570174, upload-time = "2026-10-27T22:49:51.889Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e4/8573194930c7a8cf4354b894dfd3a40ad94e9c33b5e6c88213da7038a630/obstore-0.12.1-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b8e376fb425cd3963bbc2d51c0e0f19bebdaf3f824ffdac9bfc8dc13a782b5b", size = 5257775, upload-time = "2026-10-27T22:49:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/9b3ec1f0c054b104ebf00333dc8f69f56c023c4f466fb09c52ccd25501a7/obstore-0.12.1-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:2fcec4ccfeac0c5ad43c184692f60d15b79e8db594b075c45a40c5f7eeab9276", size = 5403147, upload-time = "2026-10-27T22:49:55.245Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/834fe762603c42d70957699d5fa84ccb51c812abc60f22a70cd56ac1a0fb/obstore-0.12.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:656547972c57aee8054fedd2585f4280d50bc8fee192269367547c4a01759d2f", size = 5821292, upload-time = "2026-10-27T22:49:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a8/1e71da6cc12d4df8da701defd2ee78edfa5298f8eddfceb6b491a9c7b4aa/obstore-0.12.1-cp311-abi3-win_amd64.whl", hash = "sha256:d84cdaeeb5f45bfa685fef3ec87265b48af13f8d006ca7c5ed1424c91e448dd5", size = 5343080, upload-time = "2026-10-27T22:49:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/3e/39/6bb0099a1002498743a9aff82b6e5abbf07d0ac8ed672080374a3528b16a/obstore-0.12.1-cp311-abi3-win_arm64.whl", hash = "sha256:6eaf07332b1121dcb3b401da9220b22f94d8e3823f2cdc69e4d8b13c4adba0a6", size = 5155426, upload-time = "2026-10-27T22:50:00.462Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/ca597f403114ca8cf0f46fb15db1db68b2c04bd14203a21d8eb5491688d5/obstore-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5a8dec8c89d710ba0debc7a9e7a3d56bb207f32d6acb0dc95f786c19aa4a811f", size = 5443755, upload-time = "2026-10-27T22:50:02.018Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/42c26efb568b1a386f279dcb26d05df04243dacc5f4123108790390a3c00/obstore-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:850062a27e9162c6bb9c4e18f539a693531df741b6c5fd5d8b2a17a8c4681dda", size = 4562659, upload-time = "2026-10-27T22:50:04.262Z" }, + { url = "https://files.pythonhosted.org/packages/d2/29/341473d040251cd4918204315cbc00b0bdc9ed94e89eb3ceacd4b4b75ad9/obstore-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de9eb44136babc4eccdb96e5d04f195612b976f6e708573a127b2ac46eeb0b49", size = 5423989, upload-time = "2026-10-27T22:50:05.848Z" }, + { url = "https://files.pythonhosted.org/packages/13/5e/febe7b32fc416128d61e18c86fd5da7adf8be22b1bf0be1d94e8622a197e/obstore-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee8a1d6619ad9db9a84b1e7b769f639d16f7122255e48fec475d93f00d1206f3", size = 5353082, upload-time = "2026-10-27T22:50:07.416Z" }, + { url = "https://files.pythonhosted.org/packages/47/55/926becb2372bc22af0aa0c9aad61dcdd9af29bae942a36951c622f167c63/obstore-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8806b2e4a46a8ae69b1af7187d5bb9a0f1c717583c5525e7eccdf9b7e76da475", size = 5575137, upload-time = "2026-10-27T22:50:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/f6/94/c7050bf30060fe8ea9523dc55a2d57b73dd5567abe62becaac7deba6b102/obstore-0.12.1-cp314-cp314t-manylinux_2_24_aarch64.whl", hash = "sha256:7880d391c3edf7abd6b1cb4c9071acd2c4ea108e4af83f57df4f351f4a5816bc", size = 5363584, upload-time = "2026-10-27T22:50:10.833Z" }, + { url = "https://files.pythonhosted.org/packages/e0/85/868d3589d761d5250724566f0dd942ac4b341279821cb8bddd62fe87416f/obstore-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf0131b338f5e23224d9009dd6f67b6088aeeeac80259ae32543f5653bce9ab1", size = 5561341, upload-time = "2026-10-27T22:50:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/cb/78/ee95ddd84ecd492f132bd657f6d23761373eef496247fc1806bebfa02c3f/obstore-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8378a242379dc8f3955bf17847f22987348f2d978d0e5a43c15fff97a3c60989", size = 5804292, upload-time = "2026-10-27T22:50:14.14Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/1197446a6376c4297e162c6242dd01de7ea3419fa627cce6239cb01a8f6b/obstore-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:13f5a95b9722e4d489b9ac0a9d75d3e9d097ccde241cdfd6ca0150a3dd742619", size = 5320383, upload-time = "2026-10-27T22:50:16.004Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/34f86122a19ae305e58d2913b0eee7e777cc69283d5bc068590b6a8f68c6/obstore-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:31b5d33132e28f0933c3d35e9a1436a3b32ed68fd8bfa6e1d1b8ad69f7aa8d82", size = 5136428, upload-time = "2026-10-27T22:50:17.72Z" }, + { url = "https://files.pythonhosted.org/packages/00/2d/8c472ebe021c474c55ad79a8dfe3719281d1ac0592870d7fca8d92361461/obstore-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:2da906fb0b5c28f6389a9561b96a5cc4da92f5eb9c2aeb3e2bd3768ab87e3717", size = 5491955, upload-time = "2026-10-27T22:50:19.814Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c5/7a6cce169714f2a4c4aa90831f2c4015b07727eb8e17d858820ba70e4a81/obstore-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4095f4e70ca24e8e7b1efeb87912cfc5142e2ec82888b1a63c0caa94fe875d22", size = 4608411, upload-time = "2026-10-27T22:50:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9e/c0a2ab434103f89cfb0dcb3218e489273295936fdaa0b58fdef0d932aa77/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab20d8a422a11b4236d4041861f52fa2e7e5b53fc80f2629c21fa3dd1e7fd8a1", size = 5059569, upload-time = "2026-10-27T22:50:23.035Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/d0c6db93119f0b18730d11f43ee46730c1c1031f35a742d99955fe8114c5/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf2c08fca6766f70a4d693e752fc279b57481e0a9fbb6d86770f3eeea67ee7e8", size = 5289087, upload-time = "2026-10-27T22:50:24.627Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/f8ecff794eb93e13a749c4738cc39ffde2a227780676fc6b3e3e1874b953/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a357ef92897cec37c6c4f630c71a8b5c3b602439bfeb7201c00682527526434", size = 5469409, upload-time = "2026-10-27T22:50:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/1e/26/ad477c01bb505ba6ad648286e8ec89aab7290a089fc761d600c65609de33/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e422bf5821e5ceb8577154b62389b5b44136718f345db663bdd592d301aab1c0", size = 5390957, upload-time = "2026-10-27T22:50:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/54242ed5d9f0b94631f686118292c639ba5824c7eb6fed7321fb52267523/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd80719105c062b941b172ffe758b8cebee7ab0526562793fb2e9d57961f8072", size = 5625205, upload-time = "2026-10-27T22:50:29.604Z" }, + { url = "https://files.pythonhosted.org/packages/a5/18/94263560896c75216c24295d1f6292180d9a006b94df9cfd45640af9fab5/obstore-0.12.1-cp315-abi3.abi3t-manylinux_2_24_aarch64.whl", hash = "sha256:fc673c165955d3b91fac276c8720daa10276f31d685f1e12fa16e4bb0144e82f", size = 5407054, upload-time = "2026-10-27T22:50:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/fc/45/497e3a6de4cf78148fcffbd3bd3bc1272d9229feeece22e46db7927debfe/obstore-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6a019c276c606d930f5a7c538865186f9baa102019be0e24bfc94a8289a571eb", size = 5601269, upload-time = "2026-10-27T22:50:32.738Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4f/6c0bc2a08bf6615417ad03cc3bf6a36da2e4e456ec7a0577803a8e934189/obstore-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:a83b1ee193c32f4da0473606ef32b9e4de924e00754c404e0ce9ff82f3076797", size = 5285684, upload-time = "2026-10-27T22:50:34.304Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/b21fa2caaed5050c0622dc0ae5ccf5dc907a4ea33cf03d1f921734cc5828/obstore-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:8ed7826686178130f5f131e7b2d435f24f1256bf44029d4fb29ff1fca441c008", size = 5412047, upload-time = "2026-10-27T22:50:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/989bb53ab9371959aa8923598520ff17f30e43b2e790d0a1e0cc5afb50f0/obstore-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:6e8b45d1ceed62706c0e19680a6cb705d9094d550e7f1287e230a86694a7c5c6", size = 5853753, upload-time = "2026-10-27T22:50:37.568Z" }, + { url = "https://files.pythonhosted.org/packages/4a/95/86c53a2d63dd3521fd895bf02a312893517f1489eaee837e7c1919b92f12/obstore-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:1b902e538809fd01e938707cc785e2efdfedc17600223323ef7b6a3d5c0874d9", size = 5365259, upload-time = "2026-10-27T22:50:39.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/8a464c4d4a8ebce693e549777be90625cf01d0ba75daea99330d909d54a0/obstore-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:7c0497355e6fe55d98ef3fa94429488be9f2c4d26a786969f90a1e179efaa93a", size = 5179055, upload-time = "2026-10-27T22:50:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/0f/46/db83f5e902acc4d7188e828b52ff72e1072fcc92d56af1849fc3e3894350/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:019af6060d0143975594a3f1efda0d15c0098702b889d048a2c085018a5c65e8", size = 5027315, upload-time = "2026-10-27T22:50:42.851Z" }, + { url = "https://files.pythonhosted.org/packages/ee/37/335aa42c62ce365b2ec37355f06eb6f57a8b53aa99160f0babec7c593e63/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f7aeba8901b506056b01765e72d13af8a295ef1b64ad73251895364a843afef", size = 5278226, upload-time = "2026-10-27T22:50:44.413Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e0/b3d447736a8e985ef10ffec4061bc895f2b410fe983b683213fec404ff0f/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdbd3db63d7f97ada70ff9351948db6e4ba883ad189e422ffa03ff02dc4c34c4", size = 5440705, upload-time = "2026-10-27T22:50:45.952Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/cb10055c1f4cd78e3c52c5a43d912b3c1f5a6601d2989ea3656515061057/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ce72f778133851d27602a429e744620ea6e762630a5f58140bb4f71fdaf8be", size = 5353724, upload-time = "2026-10-27T22:50:47.409Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/bb2a98c22191b29f02c042e98be026a13e7eb284531f64d1a25a119ea6b4/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9a0399a5b43de54a31777cc56f8f2f7893ac444cac2b7d834f2bea299cad61", size = 5586399, upload-time = "2026-10-27T22:50:48.973Z" }, + { url = "https://files.pythonhosted.org/packages/e7/48/e0d510abfba83e571529b892e0d54280828b2761e6409af34b811cca58aa/obstore-0.12.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:5c1fe53af794af798ac1e0ec509a0cbfc66fe68df98d3cc1b557d4602dd39b50", size = 5375790, upload-time = "2026-10-27T22:50:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0a/c5999ce2d5d70071d2e6b45b10c98bc22bb5427d84120f02ca94b1716c7d/obstore-0.12.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f7e02534dfcc2fd6188a4aa5901bafcf032142fbf8836caab80db7b6ad25e369", size = 5569722, upload-time = "2026-10-27T22:50:52.507Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/b899283be0df446f3f9cb2815a64d9faf8a8b30487954795b92471d3e90b/obstore-0.12.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:eea60451265e609c48510787080a403b79144a45e69f59b330f7e3901e11236f", size = 5252932, upload-time = "2026-10-27T22:50:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/daed807520fdab5ac0031e0d7a459b3ce5aedde21f2f315b278c369599dc/obstore-0.12.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:f4cedcc164644f2923b19a763aa07a04134072bbf7a190637c267072cd8f6e4d", size = 5398228, upload-time = "2026-10-27T22:50:55.92Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e0/6e38c19d1fd4dc40f087ad953c2c0dea1203994d359f253e1123ae175351/obstore-0.12.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:94fba14cd7d64f7a0b4da0224dcaec9c379ba7a1ddcad2c5e4c35a0b56977b61", size = 5815556, upload-time = "2026-10-27T22:50:57.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/f0b45052e07d8eb2f5a6836b45babf18b06dae2019404f9af4662db6666c/obstore-0.12.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:2edaecb8b06e6f1674f8e0375f0e25df00c4a2162c489769cd4ffc182e8069ba", size = 5424080, upload-time = "2026-10-27T22:50:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/91046acc476a58c02731140d987cc3faed8a4dd073154b9c3eaf34965107/obstore-0.12.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:eb4fcec7658aa6929f77502edef424213db8dfe173276ae6bff67df8f95e5aff", size = 4591674, upload-time = "2026-10-27T22:51:00.972Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.63b1" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -3940,9 +3907,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, ] [[package]] @@ -3961,81 +3928,81 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.42.1" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.63b1" +version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, ] [[package]] name = "oracledb" -version = "26.0.1" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/f3/22113415f48b6608ada31ebb46047b3b8ac361dba393d1b5ed1f768d8f6a/oracledb-26.0.1.tar.gz", hash = "sha256:786397a6b37e94ebfa6c1c6cd026755eb8fccfd39e15bcb64cb5879491882aef", size = 910463, upload-time = "2026-09-22T21:23:20.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/91/e63f777028a560908bc3662b1cbbaf8aba3a27dd57304655349fcbd128fe/oracledb-26.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c8635ed9f8d659421679e16d7da56a0d50078d91b40782fb9c27c85f8d6c3075", size = 4806896, upload-time = "2026-09-22T21:23:36.916Z" }, - { url = "https://files.pythonhosted.org/packages/0e/59/149845af7f23cac1138f76ff1f68d0e6e03fe65e67da8084251c98bc4fa7/oracledb-26.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1872361b806678c8865e375b8a46b59e2b4923191fb49af37d1dabc9438189f", size = 2633349, upload-time = "2026-09-22T21:23:38.505Z" }, - { url = "https://files.pythonhosted.org/packages/e4/58/caf7db67e8c66e0e099c30b58d257e3a60b1b33c88b331dd2802c9e356df/oracledb-26.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b6949975dcd283dd4904c79c2263bbb32843116eb07f16b266b07b069ca2593", size = 2829714, upload-time = "2026-09-22T21:23:39.971Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e8/979beffc989f146ae5e37cd5b53ebe08231306afcd5ffe245368a2710a39/oracledb-26.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7d1fecfa6e7c39a3ee8aedeca7be2e48f4008d90cdcfa1b49ae9f825827b8156", size = 2685600, upload-time = "2026-09-22T21:23:41.73Z" }, - { url = "https://files.pythonhosted.org/packages/29/57/5eb64f683afa2b56ece4f8d81bd09d60db19ff487c9a92911bff2be6c795/oracledb-26.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d407685928a87240c54d2ca8cc5e0a89fa44639b459030519f120db461004a0", size = 2856509, upload-time = "2026-09-22T21:23:43.229Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c9/49e89422cfbc04fb93aabaca1008892322ea8c177136e14e86c50131270f/oracledb-26.0.1-cp311-cp311-win32.whl", hash = "sha256:391b61d248ade2f988581a2acadce20f3b3b21886481fc1e5bc57aceab7ab376", size = 1608982, upload-time = "2026-09-22T21:23:44.594Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2d/12b184980a87ed89cf6b23816b3aa6e4ed720a5708ee7af5960f56eee9c6/oracledb-26.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:582d629ca1282653f575a74b57e3c146175a55ea43c1470ddca44c3eef04dddc", size = 2068149, upload-time = "2026-09-22T21:23:46.024Z" }, - { url = "https://files.pythonhosted.org/packages/47/f2/61869d47cf3d74099d9398c56c839a21394b35dd19d05e91a60abb6e7881/oracledb-26.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:b857ecc33ed1ec8141f4e45a7420a4df4fc625dcaa35e32b01dca62400376d5d", size = 1709535, upload-time = "2026-09-22T21:23:47.737Z" }, - { url = "https://files.pythonhosted.org/packages/83/fa/349ee179b3e106f210ac070d199dd341f37e2af9f5faf2563b80d8aa6ab1/oracledb-26.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eea4d0824fa82b03c0d90aad81f10dbbacdaf351979c0130190a362a539e63ae", size = 4908265, upload-time = "2026-09-22T21:23:49.524Z" }, - { url = "https://files.pythonhosted.org/packages/60/68/2dc6a99d385835290825c0d6689097af29637db8754197040b95814bb5cd/oracledb-26.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c05707b2ba57d8210bdf062ba3419900591f3369d1532fb116af51445c2993d", size = 2448388, upload-time = "2026-09-22T21:23:50.993Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4b/ce48ed072c2328b429a6bc87a35ddd735945151145d9fe49c1e177b0da83/oracledb-26.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36cbf2be185412fee95be96aa6f187a73fd5181bb03186b53d5b70ac18fa354b", size = 2663296, upload-time = "2026-09-22T21:23:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/73166ff29389c5ce3272fcc534360ab65248f90fcdf175a99b5fa2bb8466/oracledb-26.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5ad1b6f4db3d8a43182e7734922342fac5901456b34acead92f6fc60cc59882", size = 2493289, upload-time = "2026-09-22T21:23:53.949Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c0/f6b4f82d6dc7f7d7654c0e4889caa1abbe019ec76daa1b4b4ee80859172b/oracledb-26.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aecc4cdd03cb5a5ad51f43680d16d74132ae7df65c991e7bd69bbe2ccc0fd209", size = 2694730, upload-time = "2026-09-22T21:23:55.609Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/a3ad79e18c5b27b357de005ce9d79dff1ef1646b7c55b6e6557b238f03bd/oracledb-26.0.1-cp312-cp312-win32.whl", hash = "sha256:f55d6fd9ebc3e8bf3bf4828fcd6ae152b363859628d27f92a4764111adfb1001", size = 1578393, upload-time = "2026-09-22T21:23:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/06/c6/8e2ad66bda2cda6d085f3516ad8f7834b1d7d4c9da21f96834467525b551/oracledb-26.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c6f2f0d7290b15f1a166c0d24187e7746a6f08ea6e886e521110f1bf8d2002c", size = 1970972, upload-time = "2026-09-22T21:23:58.352Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ce/98c3a83142a562e40135abb8503b59eba7af1135c11c8077986a289f3a35/oracledb-26.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c31b6f7440047c867a1b41cbaace7867db0750ecab4dc6454c9f501991152deb", size = 1622016, upload-time = "2026-09-22T21:23:59.996Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d7/2a911476339d9fa6eb83e6a4639b007c13af26d7261907ecc2d8722a6712/oracledb-26.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:339dd6c4f0d50dab4026a36ae89d902240b2a879a17cdf18e3feadc113957b4e", size = 4911060, upload-time = "2026-09-22T21:24:01.757Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e8/97e3b2c283ab996efe1d12b6afa86042c746b0ce4e408942bb4b7c35e39a/oracledb-26.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5907bb5c9df123e417ddf358f540554dfc6766064c2feb84121c099bc4e039a3", size = 2445641, upload-time = "2026-09-22T21:24:03.587Z" }, - { url = "https://files.pythonhosted.org/packages/03/f7/bbf75b91248ea489e8393ae84e9985b37c895781c658b6d9e5d83d02c1c1/oracledb-26.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16d08328bf9b02e980ad4d5f3e0e99596f9a42aaf6bd1e244ea85be753f2c292", size = 2646590, upload-time = "2026-09-22T21:24:05.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/32/cbdacb302a2ca047e36e0f804c493391844aad83822d428184fa1d4203d5/oracledb-26.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50e42111d5c620f60478e4571a5847c335286e8650e63a8789f8bd51035f90da", size = 2499129, upload-time = "2026-09-22T21:24:06.801Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/ce86b581a4f1c8d5844019f84eacc9f4084c882ba676a90c94d7eaeb749e/oracledb-26.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0453f8f1a6748c982dcdac8af09bcd81971979c7ec9d1d151e15595f58d9164b", size = 2675979, upload-time = "2026-09-22T21:24:08.153Z" }, - { url = "https://files.pythonhosted.org/packages/82/7c/49e6af631b84db68d801b6d03ff58c4fa7b767ffa8a8402de643584e9d63/oracledb-26.0.1-cp313-cp313-win32.whl", hash = "sha256:93e6a7cda6ad7b14e2ce8689c9694872b3d69d57015aab070028c9a93faf3bf1", size = 1576622, upload-time = "2026-09-22T21:24:09.492Z" }, - { url = "https://files.pythonhosted.org/packages/59/10/ba7a5ce82fda9bbfcf222930b9a6f9a967c2b3d49f787ba7f4f99b598c6f/oracledb-26.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:6cf0c4d8662072917e2f98598cb97a18e157785bd0f294fbcf770d822ca239a9", size = 1970210, upload-time = "2026-09-22T21:24:10.806Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/96ce57dad3b1bb3d3c30be936d0f6daea0a02649d8936b878921004b15e1/oracledb-26.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:22bb1e47a01a5259eead9202be8f631682b69be08ee29d8e532ed9b9364bd7bd", size = 1621513, upload-time = "2026-09-22T21:24:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e6/7cf147ea1daf0c7c86fcd77acce1a09b0d4ab75539a4677a2034f112c860/oracledb-26.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2201184b46bbec476a7ac29a72f14ee4ed75d692ade3b6c5d672fdbca71e6b2", size = 4961776, upload-time = "2026-09-22T21:24:13.967Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7e/1ada40fce6ec550718e6eb1b8be9bfcc6e9697f19b6244ed6e0b89aa8da8/oracledb-26.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bddb6bd7156df750045b419ffc400cfe429276f1a17d83913f10adefd0baa9a", size = 2486660, upload-time = "2026-09-22T21:24:16.397Z" }, - { url = "https://files.pythonhosted.org/packages/65/2a/a4cf6f8081b1ab3498c82e3917b2174843bf22e2fa4c41e98ad1e0b66d47/oracledb-26.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33b22800a9b02ee0ead00e6f3073b810f7dc51c1e82fc9fc5475c09812420df", size = 2664849, upload-time = "2026-09-22T21:24:18.16Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/50a32fa2d8c6cbb0e5fb69c262b01e7412ad08e430192404af9d2b9f4bea/oracledb-26.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:124f0072e6c7df277c561ec3a4c42cb2c02c30e9ce15e75a460b4abcfee41046", size = 2544330, upload-time = "2026-09-22T21:24:19.627Z" }, - { url = "https://files.pythonhosted.org/packages/b0/90/9b6dd9bbc1899ca3a982e9012f25be2a70e4795eacc6f8582c340c64fe1c/oracledb-26.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4c5094d5ba60b0f67ce7c74eb0fbc738d827103c0ba2db5f8483db405ab4e9c4", size = 2693190, upload-time = "2026-09-22T21:24:21.228Z" }, - { url = "https://files.pythonhosted.org/packages/92/42/0df9265f2f562283c88590a6bcd442084cef80c832ff598bdbab994a3cb8/oracledb-26.0.1-cp314-cp314-win32.whl", hash = "sha256:03424ccfbd25c402133c2a23c5c1a630e57effda7b962085d91c41d765af1b25", size = 1602133, upload-time = "2026-09-22T21:24:22.733Z" }, - { url = "https://files.pythonhosted.org/packages/db/d4/1b90c5e252a53dbc71833217746f2673e9e2ce0e06115bac1dc42ae347d0/oracledb-26.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:ee8f4736b72f38ade26f9770f7b40e8662bfa825ef48cefcc55cef0c30525d4b", size = 2027835, upload-time = "2026-09-22T21:24:24.105Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4e/9febea61cbf476c2ae5d290fb3f823f1e46668e3ac15b8af5307531e33ad/oracledb-26.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:13b0dfbbc503c6107d6c08b1658df260c2c077bc5574d9545159df0a0301fe7d", size = 1680743, upload-time = "2026-09-22T21:24:25.475Z" }, - { url = "https://files.pythonhosted.org/packages/77/bb/71f7861bfbf586757873259a858f370030a137b4172865992c3cc9a462b1/oracledb-26.0.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9e77a6b3b01a3a2f4b786939f59d862fbe73fbc4c68cdf87aa45a5398725a16c", size = 4960764, upload-time = "2026-09-22T21:24:27.244Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/86aa68f08e5c1d25b864ac5777c559bf0bc8911e5d8262b70bec5e05cc21/oracledb-26.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d722f185f9c05ecccb2702a58a1e5ecfd309ae92cad0c38373e0969e83bf2bd", size = 2488466, upload-time = "2026-09-22T21:24:29.177Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f4/af94230a3c2c6679323f3578a0a5281cfd87f72994f66e6044b5ff6c95e9/oracledb-26.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfcf6382bc48e21e448ca2b71644cc75626056a638bca50181acdc8adc69dd3c", size = 2685915, upload-time = "2026-09-22T21:24:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/f5/a7/aa99f8e60961e80795ca926ead66c66d98c3aee78d970320eacdfa843614/oracledb-26.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d42a2e0caea9bcf7d260d430b225d72cff6dffa0d6729be7a8823db2678e0b74", size = 2544920, upload-time = "2026-09-22T21:24:32.667Z" }, - { url = "https://files.pythonhosted.org/packages/e2/65/b636a18a578cffd5819df08ae80deeb931f1215d2d2b18112a37e9d2bbea/oracledb-26.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a073cc8bd74a7ce32d12f4bc898dd143971981b52ecd2f72c37b4474bff76cb", size = 2713492, upload-time = "2026-09-22T21:24:34.22Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4a/e758838b14540a94599487d0d15ce4390edba5a257e53f2cea66f94bd138/oracledb-26.0.1-cp315-cp315-win32.whl", hash = "sha256:f4188bf8eedfc05ef266835e98ce304fda92d6bc87dd776d5f9a8b510d8b54ba", size = 1601872, upload-time = "2026-09-22T21:24:35.591Z" }, - { url = "https://files.pythonhosted.org/packages/62/b1/51b2a51abd89ba43dc0f9ee14d456a9b002d0b9c31840c7de55387e555eb/oracledb-26.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:9d508d2df7bb9a4802247ec95d06064e2babf8ae9e1f91d89c349bcd30df44ed", size = 2026829, upload-time = "2026-09-22T21:24:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/b099ff2c51447cbf0d2f1c33ea7de1abf96d9bf4c3d93b2a5dff48728965/oracledb-26.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:7c9541d7cf2d324e9516c955c033235bbf25b7d96ce41a856b3bff2eab3494ed", size = 1680535, upload-time = "2026-09-22T21:24:38.551Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/db/11/96b4a08808217a230a662379b72140ad292f117b213a0274d8036aabf3ad/oracledb-26.1.0.tar.gz", hash = "sha256:18e1e78073a1479326e8766deb8b9358ae7880d80b65f562580b010da07c73ee", size = 943791, upload-time = "2026-10-27T17:16:45.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/77/f953a47ec9944f36f9e9b1d91a48c4e8025c38bd8c3d9d05820e20fed8eb/oracledb-26.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f5d798b43136bc9d33cec05abb48d46d228fa71c42581ad59acaf2911aa607a", size = 4896939, upload-time = "2026-10-27T17:17:02.612Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/1eb42dc2c753a1cbc61db0c922c69b28bad79cf8fd04aceeca2088eb4e97/oracledb-26.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76929b0bf9be6379a7e9cde9578f6526e2b0a480c51570205d39637dccd3a377", size = 2695172, upload-time = "2026-10-27T17:17:04.002Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8b/745b52b661c4c6131e66a895572367d707b7023fec6e3b9d5c78f43bfc33/oracledb-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca386a8e6dc9de6b4f4d3fcf4a82e1e44ba8e3471ccb6d5c3dd9b827416bcf8", size = 2896418, upload-time = "2026-10-27T17:17:05.508Z" }, + { url = "https://files.pythonhosted.org/packages/69/f5/36485ff11cafa2be93c54d927d8ceb3f7637cedc94ee16b99dec2aed0df8/oracledb-26.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49267964f748dfb5ffdfc983afc9eb458dcded731713cff7bf1f5eb4c38749dd", size = 2750272, upload-time = "2026-10-27T17:17:06.762Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/f2a053b267262914647677d9e6aaaf2c2da296189c7f5ab5d7bb85a598c2/oracledb-26.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9bc42b8686972072533b180a95d63b70373734eb9ff20ce4d3d290ea55e14251", size = 2925880, upload-time = "2026-10-27T17:17:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/48933a192a4431596d1a781a580df1bef74832fac4981f2601210f3f8059/oracledb-26.1.0-cp311-cp311-win32.whl", hash = "sha256:14dde5576cfb0ed0413cd632327cf02b210d449ba1720d1918807705bfd96b08", size = 1653578, upload-time = "2026-10-27T17:17:09.408Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/c05de9942a69f19dfc43707eaeed90bcebbcf2eaaed319fefa75f6d062de/oracledb-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a10f903086b940c2f1515dca6368316b7552a686bf561494ae339a62993b0173", size = 2119773, upload-time = "2026-10-27T17:17:10.636Z" }, + { url = "https://files.pythonhosted.org/packages/f7/74/735b6825837efc57ed20e65f12563b2e4aa520568c70cfb77213c12514e6/oracledb-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:fcd92d25fe3882958fd01302d3ea6c8d69c89627e0ce969396a3650fca8fc96f", size = 1848054, upload-time = "2026-10-27T17:17:11.977Z" }, + { url = "https://files.pythonhosted.org/packages/35/33/ef3f5ba74048111f2774f098b8362d3188268980113f3b076a8c699a0347/oracledb-26.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82ef76d5c6ca00d8c914d6a84513e4508de53d88030e6abe19a8d7b403709361", size = 4998984, upload-time = "2026-10-27T17:17:14.062Z" }, + { url = "https://files.pythonhosted.org/packages/05/97/2e19d7184d85867f3862a368218e59244aaff0f4bf382d30b4f3788b58b9/oracledb-26.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fccc3a99ab060addc60d8560e765bb52b752f63ac80ce42e480d0d2bb10910ca", size = 2507588, upload-time = "2026-10-27T17:17:15.531Z" }, + { url = "https://files.pythonhosted.org/packages/86/cd/6d1b47372ad75ff61c96d4949066224f038f15ac25bfc63eade528dfb32c/oracledb-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a10dc4e094d9ffc5deb68499e8f6a4a33aa95656f1e735aadc1cfd2f9e7aac0c", size = 2726296, upload-time = "2026-10-27T17:17:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f1/8566c4ed3f12b6cf94330595fc137d9d790c39830c42591a553607ed1508/oracledb-26.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7d563c30ef62f55e53c09ccd0036d83935a9621ab1476b4ac16d3dcc1668d07", size = 2555430, upload-time = "2026-10-27T17:17:18.507Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0a/a72bd37e23aa064c32e2d48dccef0d671ec43ab85cc683a9e8937bbdcb2e/oracledb-26.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc698fa8d8f044d98ac0a2ae765da63688eddd3fd0083d18a21839111e94484", size = 2755767, upload-time = "2026-10-27T17:17:19.816Z" }, + { url = "https://files.pythonhosted.org/packages/3f/18/54692e4468494cd0ba3a7fe8d0150d6c6a08bb7c48ff31f2a70c2f184db2/oracledb-26.1.0-cp312-cp312-win32.whl", hash = "sha256:f36a54fcfa6040f8a172314de35cfb1de7e7078f475ce33448b68e989ab9a786", size = 1622525, upload-time = "2026-10-27T17:17:21.149Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ef/6408f374f00501801dc2dcc19e82b15d6378905fe369638c71f397901ef2/oracledb-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:e7340527d314907d04e909825e3cfbcb16228c04ceff01458d1c359ab4741a3a", size = 2020892, upload-time = "2026-10-27T17:17:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9c/5956f7527510ef8f5f41b5c04844909143da5f5c8a980212d4da346b1977/oracledb-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:1421356bb391e23f3d3514b6e5109aae8715a1b97b26ba768bd22f907ba0f74b", size = 1744275, upload-time = "2026-10-27T17:17:23.793Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/8682086ae0399907451726d870f59a76453e77155bb97e2bf8763734d3a7/oracledb-26.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:26f1b210cd02c7a24753ac7981c0827a1bb8572c5f7f26e7b4394b948f6d3937", size = 4999909, upload-time = "2026-10-27T17:17:25.036Z" }, + { url = "https://files.pythonhosted.org/packages/03/6b/87d78ec1f2df8d5530680e4dd153d314b6bee8556ff9c21ccfa7ed5d83fe/oracledb-26.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb5ae69a25c45d6fdff7ed52dc2110b59ef156beee6f82f78c8966d8d3c35a4e", size = 2501297, upload-time = "2026-10-27T17:17:26.337Z" }, + { url = "https://files.pythonhosted.org/packages/83/76/e6822285875816c901626fbe30ac29b4220ebc604e4f4dfbe8920db546c6/oracledb-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40077ea15c242e8a05195b1c317c0ff82200d5acf304a387283ea90ab8af6e92", size = 2702448, upload-time = "2026-10-27T17:17:27.835Z" }, + { url = "https://files.pythonhosted.org/packages/52/42/485c977ff9ec44933d295facea5d408651c8d532a9e99dbd1cb4efd63785/oracledb-26.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bff0cb102bd147c9f7546aeab64300f970695fe3f3da4bb861bde58293a12dee", size = 2555181, upload-time = "2026-10-27T17:17:29.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9d/b15cb2a0b90a7b61bf6e6cc26a3a8a3091ae8525a281f96f1a1c44e312c6/oracledb-26.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3472ce37ba7f2be4702bef0f0f655def77447287a070a64994f22fb45273c4a5", size = 2735989, upload-time = "2026-10-27T17:17:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/f6/21/7e485c279dd062f2c9d196ff4a5f18cfe10029945ea995c858280bd32cef/oracledb-26.1.0-cp313-cp313-win32.whl", hash = "sha256:a280b65d55ddeeb39f259d48dd99b81b5385bce5de2d9a517ba1bedacead7abf", size = 1619889, upload-time = "2026-10-27T17:17:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3f/a2ded12389b2c668776deee582b0f82eb13e58f00e707d49a6d9d9388765/oracledb-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f34e305e9eff7dc9c4b7bd3a19a78d4932568e60070855cc057ab8d0a18a16f", size = 2020219, upload-time = "2026-10-27T17:17:33.605Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4e/27f2cb2faaadd0c24eab5f186fd403af97a1e027c6854af42fbb395a2770/oracledb-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:77f8f6c2b4dc5861f58c0b448fc1da5ebba320da8c457608b8ecf4b8d93f8ed8", size = 1743359, upload-time = "2026-10-27T17:17:35.122Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/3f60c5f6e30e3b36560967689cb36885468ed290e9f6d7fa2bb715a3c90b/oracledb-26.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e0aecc554558d739dab0c346bf18cfdb079aff1bd24fa1f069df8a93123aec47", size = 5054788, upload-time = "2026-10-27T17:17:36.754Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/e00707a669af0f7396a7784bb8819a0dbaa5ca35b4ae1f1d2bb862de6244/oracledb-26.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cf48cc91399b300c9b430e28d91dca858fa4da4d0bb9b3887302ef477d8d562", size = 2549021, upload-time = "2026-10-27T17:17:38.197Z" }, + { url = "https://files.pythonhosted.org/packages/a5/98/5a4ad92c0564fc5f5be9a77fc14f0f33d55326910b14687788cbcb126c31/oracledb-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6993fa6d9582864eff72e9909c43666e30b83b5b3c51acb5f013c25175944c82", size = 2725259, upload-time = "2026-10-27T17:17:39.589Z" }, + { url = "https://files.pythonhosted.org/packages/98/3f/dfd1dc5cdcd328e472c1855eda407078e8bce1cc41ad105d679b7424b93e/oracledb-26.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b96d28031f87a2df422730ba730fc49739844c01da951b762693cde96a95fa5e", size = 2603235, upload-time = "2026-10-27T17:17:41.159Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9a/b308177a8b3fe38d337043623ab8e1cb8857786cbce3a5f4f68be3ed1bbf/oracledb-26.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b163bd218caf65309b8513d6d6986c28afd0a3c9d697fb0bd6774e067110c2a9", size = 2752500, upload-time = "2026-10-27T17:17:42.649Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/f74cfa64cf481540e8fb7a0e428dfe2fefc14370b907719242e19d9ab409/oracledb-26.1.0-cp314-cp314-win32.whl", hash = "sha256:ae4f1d7e32e71f4ff5c67aa8aa38cb2a204c1ce4a1b6bd83114473344af12fcf", size = 1644951, upload-time = "2026-10-27T17:17:44.074Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1e/5efe8d52f7b1d50b6e8a40bcf43ce8dd4db426bee7a481246a63bc55a96e/oracledb-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:87c6ebb0cf4b1b72c0b4c22327bc9d29fa13052057adaa660e64d8eb242d4c23", size = 2077032, upload-time = "2026-10-27T17:17:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/0cfa6eda47789857fa39fb1023b17bef32e0c06355bb21c87796a9d6a82e/oracledb-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:103dd657946244b0eda1056d11f6f425301240b204e107c1d1763024a6207328", size = 1798878, upload-time = "2026-10-27T17:17:46.954Z" }, + { url = "https://files.pythonhosted.org/packages/e3/22/74400e108fa596837bb2eb981a78472043acd044b0553f70074bb6d3b8d5/oracledb-26.1.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:950e5fd97a9b7b31218344d07805eb846e1a05d206a90e0faa63428a432fadc3", size = 5052940, upload-time = "2026-10-27T17:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/41/9b/ba1abcc0513e9d0b51c49cc674831bfeecaa230cd6781d7f6f5dec620fa6/oracledb-26.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5434344431fb13d71f06e428a894f08c65622ce94f1ebdc9d2fba982515941a8", size = 2550183, upload-time = "2026-10-27T17:17:50.277Z" }, + { url = "https://files.pythonhosted.org/packages/04/84/812c5dd5dcde8759e217e0be33a966a2db115a1e1ee5352f800893557cc3/oracledb-26.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b16e809a5409f9e4e6552fe7d7f6871e52ec4bf749c1762ce3ea57c2e6e7ebb", size = 2744432, upload-time = "2026-10-27T17:17:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/31/49/97db788bbcd1edb0aef48e5bb466162b1496aa14e85e4aff92180dcd4e4b/oracledb-26.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:27921624f0fa0fad58b31c8c0c32981708e81b75c381457eef15b1ad1c711126", size = 2603456, upload-time = "2026-10-27T17:17:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f3/195e97170be594ce3a71924a11d5d304289ccd2ea5503fe18e0ca460c3d6/oracledb-26.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8a32e29e9588a886712c48a0bfa48b0f61180b620b723aa5954fa8f915a5cf22", size = 2773979, upload-time = "2026-10-27T17:17:54.81Z" }, + { url = "https://files.pythonhosted.org/packages/9d/55/56bb2bea611da121b3e99a1c9f95d4bada5638152eb36b52be41c98f070b/oracledb-26.1.0-cp315-cp315-win32.whl", hash = "sha256:31d42cadf3a15b41bd04efef20bdca55e2ce93cf39bd23cad13023c917c2188d", size = 1644343, upload-time = "2026-10-27T17:17:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/78771b0b595b9146bda0b89fa9350662138145186c0574ad205ba60957ff/oracledb-26.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:6cdaecd494b1d02d440f2099bc6b1492dae300f13377d09da6cc9255755833bb", size = 2075952, upload-time = "2026-10-27T17:17:57.317Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/afe3ad0eb706f5988235fd27b36817c89b899d5e47288ca523bdb5670d3c/oracledb-26.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:6c87de373f330ca1bdcf6b1b5f41eeee5a522fc566eb17f21f8e9d0ee53e48db", size = 1799709, upload-time = "2026-10-27T17:17:58.618Z" }, ] [[package]] @@ -4107,7 +4074,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.6" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -4115,65 +4082,65 @@ dependencies = [ { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/48/88e8d250d28efa8163294f6809a71683c7ee67f63ac7c33021c0503b3547/pandas-3.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586", size = 10391798, upload-time = "2026-09-17T23:20:20.96Z" }, - { url = "https://files.pythonhosted.org/packages/55/a6/39db5d41f3eb5d7846626312cb30e0cea48e988fc47a3725698bc931ad3c/pandas-3.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af", size = 10023718, upload-time = "2026-09-17T23:20:25.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/b7/707e966129f77ee8a39d41a41bbd989b790b3fef581c6e26b821315cba6b/pandas-3.0.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808", size = 10612800, upload-time = "2026-09-17T23:20:27.99Z" }, - { url = "https://files.pythonhosted.org/packages/63/be/dfb6cc9329d0bbe76dadda8a1113d026bb996ec76c509221368dc68349f4/pandas-3.0.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258", size = 11108900, upload-time = "2026-09-17T23:20:30.65Z" }, - { url = "https://files.pythonhosted.org/packages/1a/6f/3d58f15bbe972f3d7bfa13ee06d731785a76fe8d232c92b2655a8de14127/pandas-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b", size = 11630226, upload-time = "2026-09-17T23:20:33.582Z" }, - { url = "https://files.pythonhosted.org/packages/58/54/9b494de4a3dd92fc6eb19187db1b21afb50fdc21962f32ea1aca9f270cb2/pandas-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3", size = 12146942, upload-time = "2026-09-17T23:20:36.792Z" }, - { url = "https://files.pythonhosted.org/packages/d3/dc/d2df02854aec5d47659acfb2be352eecc691845b2f86e99c84f1010a8671/pandas-3.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd", size = 9859246, upload-time = "2026-09-17T23:20:43.976Z" }, - { url = "https://files.pythonhosted.org/packages/79/1e/2a30df0d7dede5c195300a1820b0bc21cea3aa24e4c8b6c4431565ecc79a/pandas-3.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171", size = 9106923, upload-time = "2026-09-17T23:20:46.611Z" }, - { url = "https://files.pythonhosted.org/packages/77/4c/597d588c055d4373cff19cbbc32d4dd046c7be8fadee957585b5ba9e5b24/pandas-3.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7", size = 10397411, upload-time = "2026-09-17T23:20:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/18/8f/48907c7c707b61a8e5018c32e1a3f70623209bfb59020a2f6196159d3aa7/pandas-3.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172", size = 10051279, upload-time = "2026-09-17T23:20:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/67/fa/613d867c3d9554a61bafdec6f79565c8a3e73235feb52cc4a72ad2e0fa6a/pandas-3.0.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281", size = 10303429, upload-time = "2026-09-17T23:20:55.597Z" }, - { url = "https://files.pythonhosted.org/packages/cb/67/0c0f18e38d7f2d2af8c24b3315bc4046e73bbdd4a5540405506671ad0c0d/pandas-3.0.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d", size = 10788193, upload-time = "2026-09-17T23:20:58.617Z" }, - { url = "https://files.pythonhosted.org/packages/39/53/1b57f3162501fe36687e4870e1b918a6458ca7386b173af75663ace95857/pandas-3.0.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b", size = 11323501, upload-time = "2026-09-17T23:21:01.911Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d2/b1182e8d39100369d7f13f4a125a3fb6b096fef112c46d0566c25781ff68/pandas-3.0.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c", size = 11838173, upload-time = "2026-09-17T23:21:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/c7/33/5b717af24d2f27995e51e0875a269dddd373045216e34cf62e3aa764eaa1/pandas-3.0.6-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf", size = 7118499, upload-time = "2026-09-17T23:21:07.661Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/14b3b17cd75cef4a1ee1af4234eb41cc1afe4103b98c2d80e8916abfd42b/pandas-3.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b", size = 9661439, upload-time = "2026-09-17T23:21:10.959Z" }, - { url = "https://files.pythonhosted.org/packages/3b/11/3d580a604a1e35d69f6676847bd12db7d14344bc677b717f4413e79c5d0d/pandas-3.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b", size = 8959806, upload-time = "2026-09-17T23:21:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/143605a1f6443ad50ebda78a31e5a3a10147fec2590e931584aaa5ff0a09/pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2", size = 10418900, upload-time = "2026-09-17T23:21:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ca/87f8548f73d452aab35e4a90f8b39ae303295e0f2ef0b4055c44d6b3f1be/pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa", size = 10064785, upload-time = "2026-09-17T23:21:19.677Z" }, - { url = "https://files.pythonhosted.org/packages/43/1a/d951442e5607c6e3b2462eff8f420797d428aa74b87c6ecfe4f48553626e/pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c", size = 10245290, upload-time = "2026-09-17T23:21:22.797Z" }, - { url = "https://files.pythonhosted.org/packages/50/fa/96d50e1e6cd0b08b5e2b7c838f65ae644940f75a124063380b5ef73b6866/pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658", size = 10757657, upload-time = "2026-09-17T23:21:25.673Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/f82d13a2cb703e1a8acee7e01fdc2b898d9cd0c00f07d1dfce63af43e350/pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2", size = 11249114, upload-time = "2026-09-17T23:21:28.898Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ce/8aef2e561a2f2c8b38c913c67373c65ba6748174e763d27c80271b24bd17/pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d", size = 11820511, upload-time = "2026-09-17T23:21:32.11Z" }, - { url = "https://files.pythonhosted.org/packages/c0/bd/63cb67e6903ef6d9c2871916dbcbc09d254da0fe8b870cf62e16b21945f2/pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd", size = 9638092, upload-time = "2026-09-17T23:21:34.883Z" }, - { url = "https://files.pythonhosted.org/packages/75/2e/e7b35b712edb068d382ddc8b2bea8a04974100515ba2daa22b478b265842/pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f", size = 8952032, upload-time = "2026-09-17T23:21:37.729Z" }, - { url = "https://files.pythonhosted.org/packages/75/55/1a8875395b05ccd572cbca0b9255dcd2db6e6508e632a558c1a6884b39ad/pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1", size = 10492213, upload-time = "2026-09-17T23:21:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/35/61/47ae13476995cc8a40cd609e93e7cf11f273d8692925c2903cb6d38aa0d1/pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729", size = 10156618, upload-time = "2026-09-17T23:21:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f2/cc5f2adb8d6e86a85d9fb5128f8cf205a61189336f70d1f7faf0d1b53ec9/pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34", size = 10375479, upload-time = "2026-09-17T23:21:47.159Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ba/ffdcb19be4ff6bfe7d969e7cef2c567c633df5a3a1cc1053394ad053bca8/pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1", size = 10783244, upload-time = "2026-09-17T23:21:50.367Z" }, - { url = "https://files.pythonhosted.org/packages/77/5b/e150075b2c6eb69fae896f2d9239bc6ed07db97735971d53d66de6553460/pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de", size = 11382223, upload-time = "2026-09-17T23:21:53.355Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8a/b441c587dc7355bf6e1f68a91b4f76a6c29740f0c23be3acc5d4ebbeea6d/pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c", size = 11854881, upload-time = "2026-09-17T23:21:56.342Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e9/f43410fada510b43fec09993c08f552086c3d247d3ee801a678f3cb10ea5/pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553", size = 9791672, upload-time = "2026-09-17T23:21:59.332Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9e/db14c059c21f9baa1907d436f8bf30e0c76c6288225c5e8b79a08ba8b2c5/pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c", size = 9121246, upload-time = "2026-09-17T23:22:02.123Z" }, - { url = "https://files.pythonhosted.org/packages/67/ba/bad0f8dac020ab38a8637fddab01a57a82da7a496a6e6f19590aad53ab62/pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514", size = 10920612, upload-time = "2026-09-17T23:22:05.404Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a9/b500982e9aac6d52a58da4ad3f11e14168a315b06906b3f397c427878065/pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60", size = 10570213, upload-time = "2026-09-17T23:22:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/e6ecd0073c98be8f840ac3125b955272835d7d9fd69f5944383b164deb5e/pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541", size = 10252356, upload-time = "2026-09-17T23:22:11.302Z" }, - { url = "https://files.pythonhosted.org/packages/04/f5/001e230a7a7803590d9275a1a3f7e1bb605e3a495cfe5e8d3a532090621b/pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965", size = 10655952, upload-time = "2026-09-17T23:22:14.283Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ab/bab587148a3852c96aae26c4b5f9e04ce2221801ad94e166b4fbf969ede0/pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7", size = 11274177, upload-time = "2026-09-17T23:22:17.352Z" }, - { url = "https://files.pythonhosted.org/packages/f3/32/74b48d87df2b80892d713c149abfe36d5db4de41b4eccb042a2bc07dafc1/pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44", size = 11719041, upload-time = "2026-09-17T23:22:20.227Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c6/d64b72d64d7eb0fad9fe424d34138e45dee70ddbea1360dcd0adf30e28f6/pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630", size = 10191388, upload-time = "2026-09-17T23:22:23.524Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/5e5b2ccabeca73ae2b03fc82bca3eabb7466cf43737f05ac08d591665d47/pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a", size = 9387796, upload-time = "2026-09-17T23:22:26.64Z" }, - { url = "https://files.pythonhosted.org/packages/b6/77/47c5fb0be8bdd00116814c2c40d9ec42dbeb943865ef95130fe58a898226/pandas-3.0.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570", size = 10483960, upload-time = "2026-09-17T23:22:30.071Z" }, - { url = "https://files.pythonhosted.org/packages/09/08/a310cb2fefe6d2b4623ab2150da818d93d4e73e33b58e4d163bb74243c5a/pandas-3.0.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34", size = 10149432, upload-time = "2026-09-17T23:22:32.818Z" }, - { url = "https://files.pythonhosted.org/packages/54/36/6af478ec3a26d7754555cd62c3101c589c0931b1d85398e4fa5403910a1c/pandas-3.0.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e", size = 10360981, upload-time = "2026-09-17T23:22:35.621Z" }, - { url = "https://files.pythonhosted.org/packages/43/20/5ece0e9cb79a6473216620db6a90546f578001c2bd857b77c444b27acad1/pandas-3.0.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c", size = 10776585, upload-time = "2026-09-17T23:22:38.427Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b6/cd3038f31ade5e8d2b4e1c9549d4b31e4d598469562f47142b2ad9171c0a/pandas-3.0.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19", size = 11385249, upload-time = "2026-09-17T23:22:41.18Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/05bb3003737f80375d7311774916a24d161e2e591abe8c672aed7813defc/pandas-3.0.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e", size = 11848595, upload-time = "2026-09-17T23:22:44.207Z" }, - { url = "https://files.pythonhosted.org/packages/c0/30/1c0d46acf236d19ef975e9cdd5d1e0dd54924b03f0ed8287096ad4a18152/pandas-3.0.6-cp315-cp315-win_amd64.whl", hash = "sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de", size = 9783810, upload-time = "2026-09-17T23:22:47.097Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/df3304a9c2833c4810e7f1c887b04105b24731f9deef8b5d5d04522a375b/pandas-3.0.6-cp315-cp315-win_arm64.whl", hash = "sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7", size = 9116820, upload-time = "2026-09-17T23:22:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/47/25/d5f6cce5efa17c38e4f752a875b4a26d66cfadd529cb8c81672f0376d6a6/pandas-3.0.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640", size = 10894614, upload-time = "2026-09-17T23:22:53.223Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/e1876bfdc1df06bafc022d33202b5663bd86c81df2a6140aacafd0344669/pandas-3.0.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36", size = 10546973, upload-time = "2026-09-17T23:22:56.155Z" }, - { url = "https://files.pythonhosted.org/packages/e9/27/e0a27a5a5c27f7db66657b44def9e93121fd0ad4f0808355b9898fcbf204/pandas-3.0.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804", size = 10239717, upload-time = "2026-09-17T23:22:59.603Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/4eadb7d86a921c1e8bc70916cfe601ed667c4169118d17d69958611c21f8/pandas-3.0.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e", size = 10641275, upload-time = "2026-09-17T23:23:02.81Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d1/eba72e9d905e84e79aefeefdcc6bc1abe15d9077973566643f4211636966/pandas-3.0.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266", size = 11262919, upload-time = "2026-09-17T23:23:06.038Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8a/1c5bd2642b450e374b6191189f47c49538fe41f82348a66de6f647e6ab59/pandas-3.0.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947", size = 11709148, upload-time = "2026-09-17T23:23:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/73/3d/1b142bd0d0f1326a5d98c91c955b923cd1e06b5eecb428cd9d8817fa01c0/pandas-3.0.6-cp315-cp315t-win_amd64.whl", hash = "sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a", size = 10161518, upload-time = "2026-09-17T23:23:12.365Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a3/6419c14da2adc1f09a6a183b8f91d7494d325b287f4ca984ac04f663638a/pandas-3.0.6-cp315-cp315t-win_arm64.whl", hash = "sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0", size = 9364529, upload-time = "2026-09-17T23:23:15.274Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/55/b1/a10c0d1a7e3ce44eb42635771004aa503e3b9ddde0bd39c524dda65f7eff/pandas-3.1.0.tar.gz", hash = "sha256:ecb9b2d8a817b1e1ba1fdb90c0abe9eab31b4fdf2562b7a56b86ac66ace17e49", size = 5933421, upload-time = "2026-10-22T11:57:03.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f2/d8d89ee886df6d959d7b9f483a55a0e5a1d5b034f08c32f4343a524af802/pandas-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d53387f8bbff4e7aa567619d4034ffdb1836db1e768a482851b904bbeeed8ef9", size = 11595013, upload-time = "2026-10-22T11:54:48.33Z" }, + { url = "https://files.pythonhosted.org/packages/ec/73/e5243378792b79d3d08f9a6782a0e23ae65cc8c6d2f476b9d2825946b071/pandas-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d2ca3b380f0f6cc788780430eb23762619730c11d53989f3982ec96835bfb9e", size = 11159002, upload-time = "2026-10-22T11:54:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1064a6ec84ae047379a5969f6b331bd8d57013f08b30976564780fb0c9bd/pandas-3.1.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:388fad0c683ad061baabe23a76f43bf43c1d2c2fd46572b687082cfa195b7275", size = 11758276, upload-time = "2026-10-22T11:54:53.367Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/2d264868728f7cb2add91a605bea5f2aabd3da41889ee97746a586ec34ee/pandas-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a28602573f50453b57802d282f9f8cb7663aa10201127ecbbe95ed4a41c64ce", size = 12330371, upload-time = "2026-10-22T11:54:55.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/04/790ac6114ceb45575d7d7a909126210f829978f18f4f10ea8c6c847f0118/pandas-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e6f83e44846f9386e3593965bac017a54a5b5c611ea7b836ceab7f465bb4cb24", size = 12777723, upload-time = "2026-10-22T11:54:58.08Z" }, + { url = "https://files.pythonhosted.org/packages/56/b1/0513aaaac23bc1400b831881448665565a1b9b327d3eb7c5aca2b23c0b84/pandas-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebbc0e652844e52e06d1525e5084ec1609158c086c8646aa6ccb34802102e533", size = 13411658, upload-time = "2026-10-22T11:55:00.372Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1a/e33f6637445dd4b7323aa7bdaf4cc15c383805b4a2e99f958b7e32296e58/pandas-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7704e3d7367cfae71d3f8c95ee81e178c7a90bef8d6eac30df9815010722259", size = 10977787, upload-time = "2026-10-22T11:55:02.967Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/86bd686a20a5052e15581fdc7f220098426b1982604fb545e78bdd5b5b2e/pandas-3.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:d3e660af30a6683fa57a5a2969555df117e873eaaeb418ad3b2e59249405ee70", size = 10305708, upload-time = "2026-10-22T11:55:05.046Z" }, + { url = "https://files.pythonhosted.org/packages/e9/12/b94c28870f351e9650c724e6daedf661455b348a9dca6d717c42c30fa216/pandas-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4772320b7735d96dbbef437129f840dec4175f8a18a2391bd1222ed8943825c", size = 11587014, upload-time = "2026-10-22T11:55:07.208Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4b/af8ca15261db52ab07f5d0c2d4c1bc28d822ea4c2a79756bc2606c012e7c/pandas-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc5e4e12d287ea77bbf4c4d4bf47ed152d82a990d3a6794dee7c350f887532b5", size = 11167712, upload-time = "2026-10-22T11:55:09.523Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/cd5127751e2bef3d10f07e0d856e120916a3df0df6450f759ddb48c78b1c/pandas-3.1.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b4e696ff70ed06e986feeec09356c491ce878d0df69a36c8e591b6a38afa29b", size = 11476964, upload-time = "2026-10-22T11:55:11.889Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a6/dc89cdd95e12093bc813e9b814a09f1a056be3e1304edaef9bf9dbee108a/pandas-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9433b5dc2f26ef9d9b56daaf4d41d078bf4372c5d88c42ed5b397a6eea80de16", size = 12019688, upload-time = "2026-10-22T11:55:14.246Z" }, + { url = "https://files.pythonhosted.org/packages/36/87/da14aa671da792612f15847451e731d5dc0f06f6747b76b15f197ffcce13/pandas-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cba20fd269808e224d6212273008173978033a1dee0f3f217bfdbd58775a72c", size = 12490149, upload-time = "2026-10-22T11:55:16.675Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a0/1ce996c7354ad37983955571058ba0dde585663163ab067275197183868f/pandas-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:743554ed96755ad1963397ccadbe9965d92db8777ca2ae8c1fe58bd9549d7f28", size = 13090624, upload-time = "2026-10-22T11:55:18.854Z" }, + { url = "https://files.pythonhosted.org/packages/92/72/d235f82b51a22478938a3583b68e8dc05811937543adf410f34bab76004a/pandas-3.1.0-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:ac2f833557874a79fc0b5fac4c86e402ea6498beba8753dc29ca23d08481689a", size = 8091628, upload-time = "2026-10-22T11:55:21.285Z" }, + { url = "https://files.pythonhosted.org/packages/10/72/06aefff8fe5efc5e1ca8bfb2695b648a435358f6dc224d4e40159314181a/pandas-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b8c75dbb27c7d170b25c13dd35515da4bb92d40fe8bb3bd2c69a95de2218889b", size = 10748836, upload-time = "2026-10-22T11:55:23.47Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/4c86f659f5b14790d14e17253becc93c4e3d1511b73f0ba2184f06b164f8/pandas-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:73b6cea22090b7123c78ee662a5108ff36616b0bec4fe47026f86fb04a49bce7", size = 10128898, upload-time = "2026-10-22T11:55:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/eb85f8b2ce121c21286842cf81abaa670ec3570173c512298f91128927a5/pandas-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:849d5019dead65944875ce21fbf71a2ca18e982164cd7367623cbda8567c3122", size = 11609893, upload-time = "2026-10-22T11:55:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/b327e814327b7e7e61b5c5cceafd5c0c20f0d38e94f99ebf2dd6c1cec85b/pandas-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c1e084760f3b275eed5863407d025b933d25f3247515fcc84bf664405e8bb3d2", size = 11182834, upload-time = "2026-10-22T11:55:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6d/2b918a45ad31d85f898567c948f11d0789ffe7fbadd6f487565e99d179b6/pandas-3.1.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1009f5b6819b81319cf177b4f01a2823ed54c81db8bd11cdf2375f42d064c02", size = 11421478, upload-time = "2026-10-22T11:55:33.257Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6f/b63e1d43eef5746014ae3f4786c72e0020e61b6b373a2abb6ebf1be3df49/pandas-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de6d0fc96e8551b83f400bf212611a0b3a2b389594d20a2540dacc61b21e7bce", size = 11991052, upload-time = "2026-10-22T11:55:35.556Z" }, + { url = "https://files.pythonhosted.org/packages/fd/43/2cf85145d822957a3f8959fd0a850e6bf687dac2823c612b3912b8888dae/pandas-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5611344f9f0ed51619ac7acdbf72b86f5043801a66e3f8730c8971257082e8ad", size = 12455560, upload-time = "2026-10-22T11:55:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/0cb19cea2e6afbb0854cbde0ed1345b61600e0de84eb7b73583fde3e3b55/pandas-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:39c67ed9e56ad3ac0727dc2ae6a1eef766988c762d5ce2e2a162613d81c8990b", size = 13045764, upload-time = "2026-10-22T11:55:39.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5c/ea5472e8444f5871427f3a6818725684fcdce74223ca1b22aa7ca345d490/pandas-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:07807035ad66de0d833d68f9bee05801dbf08b88d56a233b0e9fd189372c3261", size = 10729719, upload-time = "2026-10-22T11:55:42.008Z" }, + { url = "https://files.pythonhosted.org/packages/45/f7/8cdd350c67bc62865d1e30cd7add43dd631420223487ba42dfcfd0d0e052/pandas-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:54fed4a50b8321ff8d413b46b7e31a023cecefd06c7210c02aa980a9836428b2", size = 10116651, upload-time = "2026-10-22T11:55:44.367Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a9/e82d44fe20dc61dd2e15bdd17c78aa0023d860d55620e4fe82f9e644a089/pandas-3.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a8c44d40cbb6d00128bf2d8f8bb66c8d561744b6d44686b5a1a01f3b3fd1245f", size = 11675868, upload-time = "2026-10-22T11:55:46.617Z" }, + { url = "https://files.pythonhosted.org/packages/83/38/6be618d98e2cbbb99a40349717532f0c2b6794ddc62f1199e3f83e45a40f/pandas-3.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89b66823678cdd53cf97187e7cd6861402bdd147d11a383706093136a4e83f45", size = 11262553, upload-time = "2026-10-22T11:55:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/5c/66e27fb0d691e835bd55b336bdf47312a541a66a17ba4d72a988d743bb79/pandas-3.1.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a8dc7780720b9e2182799ee86279d4cc7d1fd30b230ea1b202da5b6f7e14c3a", size = 11548155, upload-time = "2026-10-22T11:55:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/37/1e/42e81484c5a6791ab1b8c52c1ff1ef0483c119f710ed58ee603d241b8792/pandas-3.1.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4455cdcc03e42c17233184e8e4ea65c3ed2925d8b1d9d7dd8e6fced3f6e43c1", size = 12029136, upload-time = "2026-10-22T11:55:53.823Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c9/a3b4fd156457f3eb2585838cd56427a113eeeba679204de435a3224b8cc3/pandas-3.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:224a9267e5fa70c16bf4f8a317435eef98387940ce6c8c611f1f68f2c3415e10", size = 12575350, upload-time = "2026-10-22T11:55:56.006Z" }, + { url = "https://files.pythonhosted.org/packages/4e/25/23a017003d792117b297bbd87e35a1bb5658509f610ecd5aeaebb84762aa/pandas-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c261c7a808982557c2917d40ffd27916769f26aacb4e6775c01e4467bcd34c3b", size = 13096142, upload-time = "2026-10-22T11:55:58.434Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d2/f4074b7d1234d112aaa23a796be1574441a9fa873707eb98312a237a6185/pandas-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:e77112cfcabb3bef7281a72170670db9a5b31409a1cd3199939abb1fba67a7ad", size = 10885958, upload-time = "2026-10-22T11:56:00.895Z" }, + { url = "https://files.pythonhosted.org/packages/25/9f/bcfc9e0ea07f5c7583e129e62cd0529747317977ac1b85878b80235bb322/pandas-3.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:6c51273b89784c9d54020ed2f60e20524169aef349bddf6cba9f5e83b46077a7", size = 10281257, upload-time = "2026-10-22T11:56:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/05/e4/b3bf0b702c0a886e4abcfc2ebdc681310498592a10d9ca0ce5db6df47b70/pandas-3.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84377fa82d0b40165379a0058d8a1a267ceb09e65160de6fcc75475d4d595884", size = 12134028, upload-time = "2026-10-22T11:56:05.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ff/9e08cf048d73f4dfdb10da859790cd1a4213465dec39ea367a606d580c4a/pandas-3.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:37f88576f3449bfd86c1052f6b42274b61d7250c8d9dcc55f9dc4de7fe9883ae", size = 11729164, upload-time = "2026-10-22T11:56:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/a0df60d70fac10006841457c30d35647ae0c93af33e420e5e047b15e058d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:756fd67995f3efb6b25f8eaea95c0e893f84e0068e2c53c5fdbb65275041d4db", size = 11413041, upload-time = "2026-10-22T11:56:11.38Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/e6742d85ba7d166a1a8a99ff9f4aa9e585fb24028522daa187859ce50b5d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d9252b57bded194d09cd5ebf18d9b4edac238b16f56984ea8281d2ecfe39cd", size = 11869427, upload-time = "2026-10-22T11:56:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/c9f4915408ef10b541f31b7812ca038dd903572c163b7be7bdcece8a5110/pandas-3.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:461b79db058639e446ddf90dd7f4804fae385acf0669f6e31321333e14cb6bc5", size = 12430367, upload-time = "2026-10-22T11:56:15.829Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/636b9cc8d30b3e2027b43036dd5a8e9e7cfdd5aa86f4454235c5ef7f96db/pandas-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bfac077874a396b7ae61a307b27aa6f0256cf31c143b7f96ac622765754cc300", size = 12945204, upload-time = "2026-10-22T11:56:18.419Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5e/5c28942fa8c6cf2d8b4e941abee43a44ba0a123f9fef6411f0319aefddd5/pandas-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1340f823a1d189fae8365de314c4d3c2ea0173c61bca66cc76a91385c64d9d3d", size = 11297553, upload-time = "2026-10-22T11:56:20.934Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a4/5991dfb2cccd233ae754f2fb37a4b1474d90595d83f6790467dc5a7d8041/pandas-3.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2bb3f8cc42fae0c0de5186778f4a4957656b9b3955aca48383abfdac44a83598", size = 10626068, upload-time = "2026-10-22T11:56:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/77/26/5cd42609d8e6cb3991242116a0e499d09f29f5bcf574f72f0a4aba13a87e/pandas-3.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:04ca88727dfa86837030c493d7512bb1c008aa4f5f14149c5f8d3ff446791340", size = 11666949, upload-time = "2026-10-22T11:56:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/97/5f/ca1ac99da700c215c52fb0f5063a7fbf14878c6e100f687704c21985640e/pandas-3.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c0bfec4d188fb0c05bba47b9f9def87624b8c545913aa3585a28f19f27ce847e", size = 11255062, upload-time = "2026-10-22T11:56:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8d7aef062e7d1f48869288e5c5ae5d109923f37ed3bbd0fdcaecc87784e9/pandas-3.1.0-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfb087db83eb79ca348047961d367bdd4bf9820d90a5945f3652c7060da7363", size = 11547558, upload-time = "2026-10-22T11:56:30.492Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/a041c786bc29935afe44806f72c0111f2643cbac5cab761007a227e12e97/pandas-3.1.0-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b5ce18612ca955e7a95d0bacc20120aedf97e2fbbebbe4de029fc24479e79b8", size = 12026259, upload-time = "2026-10-22T11:56:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4a/ce6690c7e6081c96857ad7dcbe3b978394d8db682610755919e61863516f/pandas-3.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a13199463b129363701a540c7b79586581b246b10b6a7ad682c41b268e7991d5", size = 12563821, upload-time = "2026-10-22T11:56:35.402Z" }, + { url = "https://files.pythonhosted.org/packages/75/ec/ae440ecbfa8f5ba0230541b4519c1fed0c4293ff362f18f7f353b12e2dca/pandas-3.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8e4de3fe8017e3445f1174707cd977e4e1154a704af2c060a1f648862f81acb9", size = 13086426, upload-time = "2026-10-22T11:56:37.666Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1e/6459d4b0ed9c3d72b9ab63387ec11e1e658579e702e0ef117be8b5d7d0d1/pandas-3.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:eee72bba8a72f0bf9522132e5c87cd10fce9d77440b50f65326b093ec9fb8d43", size = 10877850, upload-time = "2026-10-22T11:56:39.994Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/e5fad1828e5f923e80a8d7ba9187f687159004a10f99f95dd5346b04c54e/pandas-3.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:9ea2983fe096a3d997c8c4b9f26775fa51e7fb251b10634bd7f87e6fe3542d52", size = 10276384, upload-time = "2026-10-22T11:56:42.193Z" }, + { url = "https://files.pythonhosted.org/packages/59/76/89820c2e1950425f39d30695d2296a8c7d400c4c2d279e7618363a7289ac/pandas-3.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:84d73e0814a38711e4f2a24b272f748a0b4335169d2f9752407ae26bde22990f", size = 12107257, upload-time = "2026-10-22T11:56:44.498Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2c/af4397e6e855d2c0dcb3392ab7788087be4ef53f6a3f166daad8b83c783e/pandas-3.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d3920d9b4de89fbef76ba6b863d58a330b5bfb720c9f8f58eb9b7da9b711d034", size = 11704430, upload-time = "2026-10-22T11:56:46.791Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/7791cd269099f9fca7dc4718955cfe4bb94aedb6634b59eeffefb1ed6a74/pandas-3.1.0-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1d61e0eae5219cd024fcbc21304de759964b5642ddcdbb28d9a8ba442ab6901", size = 11378442, upload-time = "2026-10-22T11:56:49.113Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/da4013bd33c49a12eb3127771111caeed3222a985651e3465bc06f839ce7/pandas-3.1.0-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbb121205af7bc5e587b94a07b5f67b5ba687a918665b9e8748f21388a88b044", size = 11850193, upload-time = "2026-10-22T11:56:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/14/89/8c820bd6fb6ad8f0edb2877639bc7e164bfb4b8e4cdf7a2202f54959ea1b/pandas-3.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:684ef3add5a92abb99c1622f2bd04b54b8843075a266e8240106ca94ce1d1237", size = 12394022, upload-time = "2026-10-22T11:56:53.785Z" }, + { url = "https://files.pythonhosted.org/packages/92/f5/9d753d85c4ea85b6d1c3735496ae5b416d7ef6905f1845c70e9521d124d9/pandas-3.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dde9a55ecd3c4f9100ac50ab15e852e4c54fcf372d800e4495e0b2159a0122ab", size = 12916085, upload-time = "2026-10-22T11:56:56.084Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f9/4a6965677477dbbb19f8881befb01b67eb6009809db73f7f1b7a0132faf1/pandas-3.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:3fe234bfb9999552dc54c45851752e56e163218d7663897dca950b4a7c32af39", size = 11267097, upload-time = "2026-10-22T11:56:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/99/35/e60fd8228db4eb848802988dbc6e3a0f1a81c57f06ad95ebe4cd534fa655/pandas-3.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f1a93c24175129bfc9922ef748ddae1d8137c743ed835e2615be329f606fed4d", size = 10597861, upload-time = "2026-10-22T11:57:00.776Z" }, ] [[package]] @@ -4208,6 +4175,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/8d/a9c2a531da0ebb54b4a7174450e8534a39db112a141ae3a437de28420111/pgvector-0.5.1-py3-none-any.whl", hash = "sha256:ec5bcd5ffaefe6ecb2dcc9564ca921d284564b969183bc837a144604773af8ea", size = 31056, upload-time = "2026-10-09T01:50:21.614Z" }, ] +[[package]] +name = "platformdirs" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/28/1c058a9143d2304b3078f8dadcbb9dc27f7d3581b5d05157c84869c4cc0d/platformdirs-4.14.0.tar.gz", hash = "sha256:83f009c1e8c5fc36869b5171755b291887bd2cade7965233cdfabd23d754720b", size = 70387, upload-time = "2026-10-28T18:27:30.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/2b/887969ef4bb53e0cb271dc7c3f4a73d50c5d58bb88aeaae311e3340cd57e/platformdirs-4.14.0-py3-none-any.whl", hash = "sha256:1dadfc59a8a5d7565b3d9270ae8fa71c065595352c4277b41c11ee6c222c4b91", size = 35259, upload-time = "2026-10-28T18:27:29.659Z" }, +] + [[package]] name = "pluggy" version = "1.7.0" @@ -4219,30 +4195,30 @@ wheels = [ [[package]] name = "polars" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/e9/001f371ec6a1bb54893f599ceebd56e6144fed4091f09f09fec0021a9276/polars-2.0.0.tar.gz", hash = "sha256:62da109e27a19a9d36657ee25dc035c9d3f87e7bd610526fe467dc37ea7dc115", size = 778215, upload-time = "2026-10-06T11:51:29.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/da/c27daf205a824f89f71172a75ad0098c5837a5a31ddeaac7f8737ea8574b/polars-2.1.0.tar.gz", hash = "sha256:9eee488d6e10773c028753dfc8dac647fd60a708108dcb818a733e85d33df253", size = 811508, upload-time = "2026-10-25T18:00:33.43Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/09/cc33bbd5463749c116b62c204d88bed6c02a6cb901eac7adab0d38651b07/polars-2.0.0-py3-none-any.whl", hash = "sha256:35d62f3541b7a6d4c360a2e2f07fccc0c2bcbd33b0ea51c83a25417a47a3f3ad", size = 876611, upload-time = "2026-10-06T11:44:04.327Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d4/e696a46c74ec8cac6434764deaf72f7edcc2e91c2145a9b3f61900a7aa7a/polars-2.1.0-py3-none-any.whl", hash = "sha256:63c50ce3533f5a03d17c3a2a24f1d0e0c7eda98fbde4268a3cba900d34060e67", size = 911474, upload-time = "2026-10-25T17:58:26.863Z" }, ] [[package]] name = "polars-runtime-32" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/ad/dbb6f6d7070867951532bcfe5e6a648d8777b416b18cddabc07030404e8c/polars_runtime_32-2.0.0.tar.gz", hash = "sha256:b5f9afcc742b4a67eabd2c680ff0f12eb02ede9b4bf807bffabd6dbb9a58d5c7", size = 3591339, upload-time = "2026-10-06T11:51:31.076Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/a6/46e09af693326735b1fba3a8bf5a725b57136fb1342e707e75d3113c826b/polars_runtime_32-2.1.0.tar.gz", hash = "sha256:c575f5f3cdb8a5f9be65477b9b4202b062593687fe62a0082add87fbd4571067", size = 3878004, upload-time = "2026-10-25T18:00:34.932Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/88/d35dec6c8928dfbaa1cccf9b626a1067da906e792c92d9f994ca825ab2b5/polars_runtime_32-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ffb7ac6cf4e8c4a652df1951e3c3840c7c23a033603d5a9efd422fa8dd699d82", size = 52494314, upload-time = "2026-10-06T11:44:07.768Z" }, - { url = "https://files.pythonhosted.org/packages/5f/fd/2237bf53ffaff47cdf1edc6c10587a7a6444d4951150eeb08d84f3493ff8/polars_runtime_32-2.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7012d8a0201bd95638545ce8f256c0efe2c5cab0f806eb043021dddde5a9498b", size = 47930083, upload-time = "2026-10-06T11:44:11.592Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0d/85e3ed90417996fc09770be91b39979074fe2978fc15b431bf8a9459760d/polars_runtime_32-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b85bb42e6009acc9629afcc70a83473fd468694d6a30ffb0ab376c8dd1a0a17", size = 50417889, upload-time = "2026-10-06T11:50:20.774Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/e9fecfd49159da92f54ff2445883577a0f1bc195da53ecc9535c458d55dd/polars_runtime_32-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d6ac584ea2b38913784db943879412380d92e28ab9cb88e20a77ba71ba3f911", size = 54475036, upload-time = "2026-10-06T11:50:24.411Z" }, - { url = "https://files.pythonhosted.org/packages/48/ad/b2abf732697b21467aaaeaac0f3bf7eee0d89c59ce8125f1ed41b28a2d97/polars_runtime_32-2.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a6bf5e260e0a6f00d0f9181438fe9e45776df8c66cee9cba16e3675cc3888488", size = 50579474, upload-time = "2026-10-06T11:50:28.377Z" }, - { url = "https://files.pythonhosted.org/packages/7f/05/304deee59a95865e1b5e9ec7b066069b49093b81b768f473d9d3b165c686/polars_runtime_32-2.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55c26eef325b6840584d91aac232e9cf3ac19e1b904594b9b54131be1edeab4d", size = 54413293, upload-time = "2026-10-06T11:50:31.828Z" }, - { url = "https://files.pythonhosted.org/packages/61/59/8c9fd7199f7c4eb1b64e640306a946a2e4a46337b3bbb33b840972c7d84b/polars_runtime_32-2.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:7da1caf3c7b4f397fb213c984013a0c755557619a2d511899a1ff74392484078", size = 54229989, upload-time = "2026-10-06T11:50:35.206Z" }, - { url = "https://files.pythonhosted.org/packages/e2/93/43608026f38aa6ed4d22da8597706a61682ee403caef0021ce8e6dc73227/polars_runtime_32-2.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:c30ba698c8904048df4a9bc3d6c5033cc2d0a7cbb0e13f4fd2de5a1947b61994", size = 48730655, upload-time = "2026-10-06T11:50:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/b424f3946c702ff48eef14e44d35df81a25a784f20f77355f39c746528fc/polars_runtime_32-2.1.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:de243b6e5a0b9f3abeaefe542136fcdc6dd8df5be3c988a40838d075314e246c", size = 52169672, upload-time = "2026-10-25T17:58:32.437Z" }, + { url = "https://files.pythonhosted.org/packages/fe/38/07066430ac655a8cab2f9e96ea1fec6ca1f20ec47844afe117662d43ace0/polars_runtime_32-2.1.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:317314d65949f1d58c4939adf83aa0d2cfaf8da50fd4a725a2de27bde2c10018", size = 47626883, upload-time = "2026-10-25T17:58:38.688Z" }, + { url = "https://files.pythonhosted.org/packages/49/a8/a773e20b451bbaab4d8267867b7c43a0c3aa67ebb6f781423a42a5a1af37/polars_runtime_32-2.1.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:777228cc8bf9cda36dee85ba74d13a25974ad4caffc5da93e10b1da9d810ce2d", size = 50033514, upload-time = "2026-10-25T17:58:44.261Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e6/649c524252d32b24d0ba92278033823fa8e0040ee3748ce5afbeda107403/polars_runtime_32-2.1.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b838056cf365d7497971d9d9825c547766923338771c48c545b86ead2ebc572", size = 53863954, upload-time = "2026-10-25T17:58:48.737Z" }, + { url = "https://files.pythonhosted.org/packages/90/f7/2eb405bf5ead6373a33fa5dd4637fe996565a1f07ccce8911ed167673740/polars_runtime_32-2.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04cfc3db1571b74ea4145944de7feceed4bfc9b26e5b2c265e27ad3b2d3866a9", size = 50204290, upload-time = "2026-10-25T17:58:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/33/4d/d76cd42042605e079d6df91cc4fea09ed5f4cb836c72c952a17f585860a9/polars_runtime_32-2.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:af58b5f6e5cc44b578f2a93d752f87c135c3a53205b4360301bc3a9b89dfbfd1", size = 53794253, upload-time = "2026-10-25T17:58:57.99Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4e/2a419d0def2908255dd1b7318c48430319859dc92ca156e2f81ff7bcd78b/polars_runtime_32-2.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:f2fe32f48af2395cd6ec52a433a03970a69aab1dbb66a613729fd549ce92c904", size = 53926265, upload-time = "2026-10-25T17:59:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/c8/1b/15ffba3264baf21d2f04c51f54b01159cb6f4fad4e84de1404be552d601f/polars_runtime_32-2.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:0af30dd53a7725f703d0d21dd421a39e0165378bfaa41431008f286608376d71", size = 48636436, upload-time = "2026-10-25T17:59:08.476Z" }, ] [[package]] @@ -4260,19 +4236,19 @@ wheels = [ [[package]] name = "prek" -version = "0.5.5" +version = "0.5.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/11538b8e6cd7e269261feae62a2d08818fdcf8f43c3b1a253cec01a5488d/prek-0.5.5.tar.gz", hash = "sha256:16bf596768bb754779c7d42317e9ca16ac609c0cce7d13bae0829944ab40a26a", size = 570428, upload-time = "2026-10-05T06:08:59.165Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/c6/89312171d4b87b7d2419a077f84e638bee69d1b7943598c72685655f86a7/prek-0.5.9.tar.gz", hash = "sha256:47bc660a7e959860a44afb5ff3dc167d96f04d929b405020a0f2c7683d3d00d8", size = 603716, upload-time = "2026-10-30T10:32:01.272Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/77/189e554ea92baf6c1d33733b3e9d1ed819ab7d77e57eb2e3ead0bb1d8060/prek-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:50fbceb0f728b504023f6e6c9037ac0656436949d94f7a85bac5542e6c1a4a75", size = 6038669, upload-time = "2026-10-05T06:08:39.368Z" }, - { url = "https://files.pythonhosted.org/packages/63/ea/fc857c6b3f1e3fdb4d81bdca2051076de82e986cb1455a01b351ac7a5cd1/prek-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f610927b95e84b28173a4c810892a7b5d5bb55bee792ddf97178fa97654633e", size = 5548955, upload-time = "2026-10-05T06:08:41.428Z" }, - { url = "https://files.pythonhosted.org/packages/47/c3/2bab54f6393fb9f247225681d476086e24fa6242f329e9ca1e5bd638ec50/prek-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:120ef7d4b6bb58155e74827743c266e48501efb0e4c37ff726b14fe6e86a126b", size = 5848228, upload-time = "2026-10-05T06:08:44.051Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f9/1279e1c83819de5904db1a09f4afc589932a2e62c7063713b1fe3a54ee46/prek-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f6c453a12c21757b23bbbc97415aef78527841ad3f9e61f52cb35cf012f93e5", size = 6371296, upload-time = "2026-10-05T06:08:46.278Z" }, - { url = "https://files.pythonhosted.org/packages/93/fc/e566be5919eaffef4cc22488f16e4bbfd08961734bd67c06f85d405e4cbc/prek-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73d509c0d9556171de2502bada4962be7a25871e7b23e782549074e1416dc850", size = 6269922, upload-time = "2026-10-05T06:08:48.645Z" }, - { url = "https://files.pythonhosted.org/packages/d2/39/a48d8e6203055bff7bb734b1f2f9aede4a4539bee6ab4c9273dbc01fa546/prek-0.5.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:76c867fdbd4e4f19e6b6b2988bd1e1407d1324170ebcf1428e4e34f8f0751a99", size = 5861518, upload-time = "2026-10-05T06:08:50.808Z" }, - { url = "https://files.pythonhosted.org/packages/b7/03/690fba9543033e25ea2e7b2ae1cca1ae8d8c2e32295f0d66f4b50934b8c7/prek-0.5.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:c69f60c2b3b4d481e38d0eab3b97ac05ec05d7648ae690925ae95ca9d22c7643", size = 6383685, upload-time = "2026-10-05T06:08:52.868Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/1e9ccf3a61a3bfd4107b83973f7afde9177b6ff8f2d5d4b00125e77c2100/prek-0.5.5-py3-none-win_amd64.whl", hash = "sha256:15bff9039349466ea9bf6b0c173b822ff08f8ae4cfbfdea782492e519c06d874", size = 5784759, upload-time = "2026-10-05T06:08:55.056Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/bb6811e438d3c74d26cb5d72e6343606e0a7a6283fee87521189f4ba1191/prek-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3db3191d2509830c75438e20956a47c2259867d18a4106064ed75088b968a0c6", size = 5517355, upload-time = "2026-10-05T06:08:57.193Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d1/318ff67aae2307cf1db65a5eea46ed891b340efaa75d9fba3c4307247ee3/prek-0.5.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:007e406e38a247fa52edddb7f7381a5c19058872c7999ee0ba92a112d768a2b3", size = 5861967, upload-time = "2026-10-30T10:31:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/5d/17/cef855f5a9c40d588557bf7a703bce0420d9592fa8407d2320df9a48135a/prek-0.5.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db03274472abdc033fce78ff28d6abe221eebd2388f0c6d293376b1b09df0978", size = 5511607, upload-time = "2026-10-30T10:31:46.06Z" }, + { url = "https://files.pythonhosted.org/packages/34/b6/ef0bfb91af9c1485775087108810eb7ea28152de2e7ecb7485310f259a4d/prek-0.5.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cc050edef6217dc0a12bd3202258b1449f24efcfa227cbf8cee7cac0a2b7f8df", size = 5625679, upload-time = "2026-10-30T10:31:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/03/01/92719c905cf39683fe3db9664f1520ed6a0ee21e899fd9862ea6f72351c8/prek-0.5.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da36f09983a5ca9c36afbda2c8b45e567b3f597acac38de9636850cbf5778570", size = 6058394, upload-time = "2026-10-30T10:31:49.995Z" }, + { url = "https://files.pythonhosted.org/packages/7c/de/9f538470c56f0524215d5ede21fdb092b5a5a55ee8b4ccc88a35487d57e0/prek-0.5.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef04fd7b4bae3a31e1180bc87e877a3a5584063fe79320a125267afee47090d", size = 5942047, upload-time = "2026-10-30T10:31:52.132Z" }, + { url = "https://files.pythonhosted.org/packages/27/ba/f4ed6bd2624f0889d4b557c72355e69636ac141c4b6c91b6af6999686b55/prek-0.5.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:db754a188464f4f3ea2101c74593ed0a5c371132c2da42daa8b94a67a9f15331", size = 5625016, upload-time = "2026-10-30T10:31:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f2/469eb2cd0e2202117104aeef5e4430c05ad3442f8a7616423e204e58214b/prek-0.5.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:7c4acbfded9dfdba5bac0690c92b1aaa5fbeedceb2172eff2c4f38a750a68075", size = 6092336, upload-time = "2026-10-30T10:31:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/cf/1e/2c35ff3b059b6fbb43273c3ebe996b6d9d37c1889607c5ba6b9eea66530c/prek-0.5.9-py3-none-win_amd64.whl", hash = "sha256:b8fe1a01273203932b46318ebf304304838b6b73ff99b89531f42c86b63b86d5", size = 5501419, upload-time = "2026-10-30T10:31:57.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/672ef3834b367ec376261fe269161851377e32597a52b447e3f1534b6f3e/prek-0.5.9-py3-none-win_arm64.whl", hash = "sha256:a548c0744311bd5ca521ea2cd845ced65b4f0de0c0a59dc87f0e1669a1c2dede", size = 5444009, upload-time = "2026-10-30T10:31:59.685Z" }, ] [[package]] @@ -4723,11 +4699,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.4" +version = "0.6.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/7e/3c0da41061a2021cac7cd524fd57092adbe5b515e8bf9005702e366802e2/pyasn1-0.6.5.tar.gz", hash = "sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47", size = 155152, upload-time = "2026-10-27T06:32:49.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/19/1f/3eefd5c84c35cae44549ce1c7f7e58f7945b34f5a6bce07a135438522073/pyasn1-0.6.5-py3-none-any.whl", hash = "sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587", size = 84807, upload-time = "2026-10-27T06:32:48.045Z" }, ] [[package]] @@ -4744,16 +4720,16 @@ wheels = [ [[package]] name = "pycparser" -version = "3.11" +version = "3.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, ] [[package]] name = "pydantic" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4761,142 +4737,146 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.50.1" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, - { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, - { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, - { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, - { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, - { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, - { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, - { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, - { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, - { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, - { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, - { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, - { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, - { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, - { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, - { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, - { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, - { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, - { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, - { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, - { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, - { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, - { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, - { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, - { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, - { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, - { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, - { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, - { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, - { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, - { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, - { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, - { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, - { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, - { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, - { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/0f/1043d3840b7859cc19abede795e058a785ebc1fa011b022b0a69514dd5c5/pydantic_core-2.50.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72", size = 2160886, upload-time = "2026-10-29T16:31:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/6211dfe1794bde5ce14f3de2212551196784c67411ac5249d8aa627e3e23/pydantic_core-2.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285", size = 2115730, upload-time = "2026-10-29T16:31:49.449Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/50a1a9721cfc21d360916754096c90467fa21386ef16875445ea06a4cf6e/pydantic_core-2.50.2-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc", size = 2274234, upload-time = "2026-10-29T16:31:50.668Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/22349c3114a3b3c400333ba6ec1d2e127fc9a4e4682440d8e23d954df0d1/pydantic_core-2.50.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680", size = 2116001, upload-time = "2026-10-29T16:31:51.887Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2e/3b88612239e87cfddd370362e93b18a50d5075883817d6b35e8e3871af75/pydantic_core-2.50.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80", size = 2204318, upload-time = "2026-10-29T16:31:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/a735bf3967d0fd73b512705d6f80fbe179d3b6208779721e86613cdf736d/pydantic_core-2.50.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179", size = 2392365, upload-time = "2026-10-29T16:31:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/9d/16/0a0febc06b311d6ca43dd7fa068273d2c4d3c4ad86ac4a6c2f2cbe924c23/pydantic_core-2.50.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd", size = 2443963, upload-time = "2026-10-29T16:31:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/22bd04c22adae266ae5e99e4c34bd882daf6c93c35bda2f9674421c1e7c3/pydantic_core-2.50.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780", size = 2236349, upload-time = "2026-10-29T16:31:57.837Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/77b12031179047a292128eee12cd7aad899d82391157356a581117e232e7/pydantic_core-2.50.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf", size = 2272702, upload-time = "2026-10-29T16:31:59.287Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/41f674077b08b79bf2f2ae3c777a5b7b8758647433c6c621580b612e8c1e/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c", size = 2311296, upload-time = "2026-10-29T16:32:00.722Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/527d1d29cfb8efc7c7f9ed9f35ab5b16a33b1837b186c6e42ecbca699817/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a", size = 2481042, upload-time = "2026-10-29T16:32:02.423Z" }, + { url = "https://files.pythonhosted.org/packages/57/35/79877aaf083424e67c870ba07c0b5ed36a2096a0cfc8b058c1d921797723/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d", size = 2488007, upload-time = "2026-10-29T16:32:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a627e40690c21be388c4abfddaefbf589e359614456156c90432ac0a76a/pydantic_core-2.50.2-cp311-cp311-win32.whl", hash = "sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960", size = 2121376, upload-time = "2026-10-29T16:32:05.053Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9b/d21478003682505aad9619ff9e086bd29aced2615b23d5315cb37de14cad/pydantic_core-2.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71", size = 2238388, upload-time = "2026-10-29T16:32:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/670a52ea740fd60f5e0a873b74505714c4bfd04c4344f789310005c4cbbd/pydantic_core-2.50.2-cp311-cp311-win_arm64.whl", hash = "sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb", size = 2185763, upload-time = "2026-10-29T16:32:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/ec0384d2a74415aeef7fcd032d29af84a98da0ca25cfb4937b10a2ac78eb/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed", size = 2154528, upload-time = "2026-10-29T16:34:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/46483477921934aa31eec9cb86ba95d2f620f4c71da749024c2e3b6e8203/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0", size = 2072173, upload-time = "2026-10-29T16:34:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/4a/16/c1eb6bdd67c9d5f42b290abb86948d8de288bdf4aeca06c588c3cfe6100b/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af", size = 2116695, upload-time = "2026-10-29T16:34:31.103Z" }, + { url = "https://files.pythonhosted.org/packages/ae/67/ccd8294730d9335c687facbdfd77515fa3b323e9bbb042c5d63addc6f5f8/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7", size = 2263561, upload-time = "2026-10-29T16:34:32.901Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/18/9b/d240307fd6882a93a88bf038ff07e1a6dcac0e88f41951fe92c9b614f0b6/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf", size = 2157909, upload-time = "2026-10-29T16:34:40.355Z" }, + { url = "https://files.pythonhosted.org/packages/30/e5/d336fb654f8f1584b4a20d71f92c2a966419bc5eafffd36848d8cb1a916d/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f", size = 2075773, upload-time = "2026-10-29T16:34:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/18203f64a405f11c3e4d569c9f49ad7a0b5901cd5fa2dff2436d28531c60/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f", size = 2277290, upload-time = "2026-10-29T16:34:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c4/a4e0ae45ad1554c38caa93e200fdd08e74d2c0149b4242c78b2475903379/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d", size = 2265597, upload-time = "2026-10-29T16:34:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/304baf7724ac137b921ce793e4b686cf4cddc431da4888e5419e7bc7374f/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8", size = 2289092, upload-time = "2026-10-29T16:34:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/0fe5ac160718d75f2f9729bb31aa9a349dadd0d32886b8c0ecc253629c46/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b", size = 2458417, upload-time = "2026-10-29T16:34:46.827Z" }, + { url = "https://files.pythonhosted.org/packages/43/44/042cbd7e46f00f45aea36496ca8bdb6b1136c2ec4063875a25286c2ba8fc/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0", size = 2492296, upload-time = "2026-10-29T16:34:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8e/e8aef16e587215a1795dbe991e41006003aef1f1b6f554361f3a6edd585a/pydantic_core-2.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6", size = 2323090, upload-time = "2026-10-29T16:34:50.008Z" }, + { url = "https://files.pythonhosted.org/packages/21/36/d083f973e28c5718f9d9d819c0b8171df1526124e8c3709bd55ddf1d4244/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9", size = 2157909, upload-time = "2026-10-29T16:34:51.579Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4b/e2eb5b51f46411ceb80451a7bf4aee215fd84012eeba22a183b4699006b5/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe", size = 2075771, upload-time = "2026-10-29T16:34:53.469Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/e9703785468d1e644f19d602985b722ff97cc8ca67389e2d79f8fb562cfa/pydantic_core-2.50.2-pp311-pypy311_pp80-manylinux_2_28_x86_64.whl", hash = "sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d", size = 2265708, upload-time = "2026-10-29T16:34:54.662Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/079d3d36aa6ee0333847d968d91ff185837541741d3c58ed0316103309bf/pydantic_core-2.50.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848", size = 2323087, upload-time = "2026-10-29T16:34:56.176Z" }, ] [[package]] @@ -4914,7 +4894,7 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.16.0" +version = "2.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -4922,9 +4902,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/3b/a5d2294799b53b448319978cfb5bd139d5a9d45e862af91661614f14c922/pydantic_settings-2.16.0.tar.gz", hash = "sha256:5b6c578049ede4db0e2ef3b4eaa4ad4069cfa9211f83fb38df899dfade50a614", size = 301031, upload-time = "2026-10-14T12:44:09.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/31/21d21e0e38a8f5f7a7f0cde22820117b6d17d6bc36f348129527f915d7f4/pydantic_settings-2.16.2.tar.gz", hash = "sha256:3dc08ffabed2bbd5dc2dd55ead7b78b4e4c5cd776aecd04310db5f1224caaf46", size = 309225, upload-time = "2026-10-28T11:14:28.066Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/f4/b987bf8c51e5b19a95fa66d1ee596074141e085d9c2ddf97920803c7029b/pydantic_settings-2.16.0-py3-none-any.whl", hash = "sha256:7e73acf7f61936a15e5a3b6eedaea29f133357faf7272f2607ba479b049dd7f2", size = 80364, upload-time = "2026-10-14T12:44:08.233Z" }, + { url = "https://files.pythonhosted.org/packages/39/88/4131c51ff87b7cd7f82a1194d0c0a2b7ca68c72d9da04c3b7e69ab91a79f/pydantic_settings-2.16.2-py3-none-any.whl", hash = "sha256:8d523e5c129b097686bde019bd6109bff53f4c8c3f63f1ddf9933ffa42e5ab4f", size = 81994, upload-time = "2026-10-28T11:14:26.716Z" }, ] [[package]] @@ -4936,18 +4916,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] -[[package]] -name = "pygments-styles" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/2c/3886ed4783dd78bb62ccab7d43380f526a7e2ff0db8c77d9c87559b2f5de/pygments_styles-0.3.0.tar.gz", hash = "sha256:67746b8fc6ff72c1179ca4d9a8bc89c7f54c196c2ff9d087f07392cd6fde3ecf", size = 15258, upload-time = "2025-11-04T13:15:23.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/64/7e0266f0c541e26df86c31d2add9be3dd9914ae83785ce0aba7cbb693667/pygments_styles-0.3.0-py3-none-any.whl", hash = "sha256:c6c45e9939eb7590345bc9084113bac46c45f12b009d13422be02e80e84a034c", size = 36617, upload-time = "2025-11-04T13:15:21.989Z" }, -] - [[package]] name = "pyjwt" version = "2.15.1" @@ -4962,61 +4930,81 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/fe/ac50ee24a80a1b344e643f6fb626063cc7041b978cd1dd9a7f9963ae1cd1/pymdown_extensions-12.3.tar.gz", hash = "sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67", size = 870532, upload-time = "2026-10-14T22:40:43.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d0/758afe732c2da502c051b4d03ff88daa3c0fc1a1e671e6c5a6318177bab4/pymdown_extensions-12.3-py3-none-any.whl", hash = "sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74", size = 277597, upload-time = "2026-10-14T22:40:41.543Z" }, +] + [[package]] name = "pymssql" -version = "2.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/3f/5d66aa70242731bfc6b6c62b14e4c6e9db0cb2dca778f0d24411b32bf88a/pymssql-2.4.4.tar.gz", hash = "sha256:6cd20778ca0e01bb5783c6d65fd2d2f2f5ca520015a4f86930d2a58ac72a0e3e", size = 211055, upload-time = "2026-10-10T04:09:49.763Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/72/b7f6f1caf55a4b0d96fc13040a66c05b36b84e8f7aeb8e1de649b57d7064/pymssql-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:db24af7e4185d4219b424617c7d8f831423ed8b48929d1f1d43cccd907a87b75", size = 3210125, upload-time = "2026-10-10T04:08:18.794Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ce/5c290dd5eef7ad579e1a4b0dbf622fabbe2485505d96f69087dca24c0286/pymssql-2.4.4-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:d6ccd0e323ece21a14c1364065fe9bc3176dbd03870557bd974ad37e80961830", size = 3014314, upload-time = "2026-10-10T04:08:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/37/34/5f99945ed455d829ce93fda685ddc8a54b2941664d588342db323689c5c1/pymssql-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98936558f5a588fd26b89e020f80b8e7c7f8d7df3a8e239405dcedf5de88dea", size = 3067447, upload-time = "2026-10-10T04:08:22.24Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/f193fd02c19f98b539180b7cddf460f960c93bda227e711e26f887e692a6/pymssql-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8035874c7d12b83c54931cc014c761229fe480c30fc0793c0b7966496c80f530", size = 3246650, upload-time = "2026-10-10T04:08:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/a5/61/f3710033a34ee7fb56558f9454ad8b39d259496d3026dd19e9065a57ad74/pymssql-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfa2504e280e54961508de51c7e7122ef309c74d609118e365821a444220df61", size = 4459721, upload-time = "2026-10-10T04:08:25.377Z" }, - { url = "https://files.pythonhosted.org/packages/c9/76/905dbfc6829e0521872016749c5f1998ad19db667f29d59477f0cf366577/pymssql-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e459565167cffb0f2a82aed3175cd7e50e6697457bf863f7b82dd00cc4993032", size = 4196191, upload-time = "2026-10-10T04:08:27.107Z" }, - { url = "https://files.pythonhosted.org/packages/61/04/d55524256a8a78631cff38f003494d04677a6dd736569403359ba4a8d023/pymssql-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6ae241b1f57a543111cde534f3e379c2df1ea3064ecb16e1fdd4200388dd9948", size = 2324698, upload-time = "2026-10-10T04:08:28.87Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f9/e5af55e9c2a673a746f283fb34539f78ee5e4f5b2b06de874811f781dd34/pymssql-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:860446859caba331dd3b2580f2f384f64f3c536a6b20c1b90261078b22868ab0", size = 3192145, upload-time = "2026-10-10T04:08:30.516Z" }, - { url = "https://files.pythonhosted.org/packages/14/c1/088c959221eaf4ac12a05f02f034b87b5cc4f190ca42cc3d54993f14b177/pymssql-2.4.4-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:50d74b1171da25b0d9bb09f0186995e28bda02a65f95ccf4a3fcf28b204943a1", size = 2995170, upload-time = "2026-10-10T04:08:32.279Z" }, - { url = "https://files.pythonhosted.org/packages/84/f5/b15e5ccdd6564a73820b536511b7a3862c25fba1a24744655a18b9b1104d/pymssql-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0090e3684cc6a1198c84cb59945f6182806c2fdaa24442e6a0b06154d5b3a31d", size = 3097963, upload-time = "2026-10-10T04:08:33.905Z" }, - { url = "https://files.pythonhosted.org/packages/2a/00/3e6e3fd8a8e8a3a68a1c21afc6447dc5551f30fa496c726b07952d893374/pymssql-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f887aed9155f0ba8ba49ef06cdf259a7c0ebce329c978930dcdcbbc901ca4d98", size = 3275346, upload-time = "2026-10-10T04:08:35.342Z" }, - { url = "https://files.pythonhosted.org/packages/81/4f/9d20fb679200ddbabf57e001019646080832bf2251c32d75460439860353/pymssql-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e8d5e01fd4ed17274e0005082ce47fae1993681a1b2b05beceb9ef77d130579d", size = 4488379, upload-time = "2026-10-10T04:08:36.821Z" }, - { url = "https://files.pythonhosted.org/packages/a4/2b/306a16194937d5ca36d2dfb3d13ef06c7085f2ecc0198ba83e101bf9da0f/pymssql-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd67b469836dc69979b13d977724377942611e8b8e4aa6896ea7a53139a15a99", size = 4223503, upload-time = "2026-10-10T04:08:38.328Z" }, - { url = "https://files.pythonhosted.org/packages/60/0f/2146384af096f9ff0c23f30f7b9db57b741cd52d1db6940835e52b2f9584/pymssql-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:af196a9f57e06745e4dc956dae801faab853240836b9047d9068c1d4020b6b69", size = 2313364, upload-time = "2026-10-10T04:08:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3e/a133fad9e5d0e40a5d3d8826ff073874f6335e89d234a70b110464f139c7/pymssql-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f255f76f543d90eb0b471e252b0b0e2858033138f20f5ce261a9195105e6ed7e", size = 3192233, upload-time = "2026-10-10T04:08:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ae/1fa071f438ed7f414861afd3732ecfa1268b3e6a0317053ba1bc76e3ff61/pymssql-2.4.4-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:4fdaf0c55c49be5488ba515788b2d36a27763b0b8b345aa483b9fd3bd9a95563", size = 3000180, upload-time = "2026-10-10T04:08:43.322Z" }, - { url = "https://files.pythonhosted.org/packages/3a/92/c9626cba839ae336a8a120a626d4cc533d53f52ab10b68734bdd55da7fa5/pymssql-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19a5af6797232e9c4eb396f86bc3779ff237b224e2eadc81486699762f36d62e", size = 3097494, upload-time = "2026-10-10T04:08:44.875Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/6070333af8ce801b884e7c9a0050b4d5278f2f563cfa405f68a94e711bf1/pymssql-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12150aae35d961986d1ffa6550cac70349798ffefea9d4403a9a9943e1b6d394", size = 3274897, upload-time = "2026-10-10T04:08:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/c71d3291e03b3e07dfe8fb104dd3a4520fc61fc2ad5f7b3385b0e0ca4be6/pymssql-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e6c0d774d3aad54df6f7c428dad346f718704eeca9ccc270f8e4499ecc2e9f39", size = 4487921, upload-time = "2026-10-10T04:08:48.389Z" }, - { url = "https://files.pythonhosted.org/packages/b7/1b/18c887fccef26fd89ec2c233fe5e6606047e6e5ade9480897208225011f6/pymssql-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:59e47602c4c9c5c221cba8bc9e44f1cbe0343ec23dbce63243065a4b7979e4dc", size = 4223072, upload-time = "2026-10-10T04:08:50.212Z" }, - { url = "https://files.pythonhosted.org/packages/f6/88/5b7b335153a0f151852e8633b672d7e6ae54c38dc1e7d13cafa929129d02/pymssql-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:405823931c3dc51ad5442f8a04a0e17cddefa0c215684605d9e5c137b6f758a9", size = 2312567, upload-time = "2026-10-10T04:08:51.642Z" }, - { url = "https://files.pythonhosted.org/packages/d2/56/cc79d0f1c41c4ec5f70213bb1be60da1deab85c40ceaf36d4b9a4f8c271d/pymssql-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a8a35ffddbe3d5320fd5b113e9d3cf6f4876be2ca155c3618a048850c6d420b4", size = 3195642, upload-time = "2026-10-10T04:08:53.097Z" }, - { url = "https://files.pythonhosted.org/packages/c7/bc/f56629a6c873d2d7a9737502c2544c4df43ced7a18982f71b11895bb3851/pymssql-2.4.4-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:c070f5b97466daaae2dbd55643fe3de9951bb736e80aaaf82a06e0e6d55a19fc", size = 3002116, upload-time = "2026-10-10T04:08:54.534Z" }, - { url = "https://files.pythonhosted.org/packages/c1/36/185dca7f4bb691d77ff853258bd7c7d55cd44fb8f44a5b79ab4df09883ee/pymssql-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b7a5766d4b2898e791646196af4ad8618a601029396dd13bf1317280c525599", size = 3095511, upload-time = "2026-10-10T04:08:55.93Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1a/ea7c498f7736209d27eb99f63eaad6121d21b25b19ebfa5f559d27d37642/pymssql-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc0fa1e68694d15ac8fee064cf6674053eacd51892fb8538cf3a703cac0a561c", size = 3273070, upload-time = "2026-10-10T04:08:57.345Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9b/18d141d421bf28cd3ea39c24cac45610f26c448efbfcc1cb4723a4e4ceba/pymssql-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3f039045feec1b0c60c9266fb27e3d39b4bcf8e69eee7485d7c4ead7adfc56b3", size = 4487159, upload-time = "2026-10-10T04:08:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/42/9b/87106c146316036a5cc19b2b2bf8d33d3b207f45f30f9525b6650e2ab05e/pymssql-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f5731261eec9761ea76fbb517834c2ee5e3ae6f689e8bc517923264a4886a184", size = 4221646, upload-time = "2026-10-10T04:09:00.269Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/f954af0ffd773731135c87add639ef01248b7646ad670f4db08db4c444f9/pymssql-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e86cb1d1ae4c6fc27fd0c0c3c2409d15c3bb9ba2dede52c4633eb8209f2081c4", size = 2375068, upload-time = "2026-10-10T04:09:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/3f/cc/523a6b9f7e6b26b86cf0ce9996f51c94082fc480a028ff9ed44e86b900f8/pymssql-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7e6b0105dc5c3648095c528ab08704f49f0cdd3f3f6cbca8a3c317f11aeab281", size = 3223425, upload-time = "2026-10-10T04:09:03.782Z" }, - { url = "https://files.pythonhosted.org/packages/ac/95/e45755d00a99299030ac2c17f09bb0cbad492b41c58f006a634246b90109/pymssql-2.4.4-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:8dc95ccb06863883015e8340cfb2c7e1ca889ab789c7109326de3231412149a4", size = 3015996, upload-time = "2026-10-10T04:09:05.584Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ae/36e9ee73e1550bde1ebd106bca02df266758e939f9a9d3b1ec5932b338cb/pymssql-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ef6906a637106141f316f3215b9772104da52409b3b1b860baecf086ef9365d", size = 3089543, upload-time = "2026-10-10T04:09:07.9Z" }, - { url = "https://files.pythonhosted.org/packages/95/39/62da8c248f65b5914174d6380f4f61ba398a82303c0de24ae11c79a7da1c/pymssql-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae93ad536e7b0b912e0356354ffef7d0f2688251f37d28ec31906c59dae7e088", size = 3269139, upload-time = "2026-10-10T04:09:09.278Z" }, - { url = "https://files.pythonhosted.org/packages/ea/02/3c2ebfe8ac54ef2eeac7e31b91133731497abed7ab2ef21276be04d5358e/pymssql-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:484f02d2ed27c47d90d72a2dde50afe6d1274e6698f38538dffbd53bc4701ca6", size = 4481795, upload-time = "2026-10-10T04:09:10.89Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0a/cd6e9364baa39fb4826e1bf563671fa12d50f39455bf14d3dc220c258c17/pymssql-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f9e423685beb39f8568c6bb5ef7b6945bc10289e0369e66e9498a85255924aa", size = 4219628, upload-time = "2026-10-10T04:09:12.495Z" }, - { url = "https://files.pythonhosted.org/packages/76/43/e20c9cbc72aefa7f5111daa632a38f24ee05ab7c09b9d8f27171e87ae1a2/pymssql-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4404ff0d9e7b0593ad7039b037a01d6a34d7b9797017187f197384ac24b981e0", size = 2393053, upload-time = "2026-10-10T04:09:13.99Z" }, - { url = "https://files.pythonhosted.org/packages/13/98/baefb5f3fc477652b57306d71abbaffaf0ca2974d6b95d16517b6987b622/pymssql-2.4.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:4d46d723be19f1a103979d2d42ab720b218be8c536a757cbe017a4a82141d274", size = 3194650, upload-time = "2026-10-10T04:09:15.74Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/7e5ae04bc955af9b19d5ee46890763ba8afc466027ee80c8e4ee67a576b8/pymssql-2.4.4-cp315-cp315-macosx_15_0_x86_64.whl", hash = "sha256:72b3f34b5239dab8d87e47ad025cde11fea80fc48dbd3951a9bc5fcbe03b1ec9", size = 3000838, upload-time = "2026-10-10T04:09:17.244Z" }, - { url = "https://files.pythonhosted.org/packages/99/4c/203448fc30c13537bda345113bf3dae9e0546ee6c60d89e6ec2d1828a571/pymssql-2.4.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0155c4bd8751eb6ca89c07dd0cd6a2b1a7d063efb6048a01489527239e10c5f5", size = 3095778, upload-time = "2026-10-10T04:09:18.965Z" }, - { url = "https://files.pythonhosted.org/packages/69/a6/c36c1c33c2a97131438eebed6cb306627be49e7f5af0b2c6c83732f6ed08/pymssql-2.4.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3dbc9f285a938f8642c8a138bcbb4a2ff002c5412521cc4093a149b21366d611", size = 3273194, upload-time = "2026-10-10T04:09:20.558Z" }, - { url = "https://files.pythonhosted.org/packages/ee/db/55bf702c56498baf6ea7251dc662927c0dc68c690389cf7b5fc335cad3ce/pymssql-2.4.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:604681336a1aeb716f3decbee51edb7de0d9c81402f18560e4d575787df61921", size = 4487327, upload-time = "2026-10-10T04:09:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/54/e4/9bab285d4dbf7f062ea28e5e2d9b25aba3d560601c48e65bf2d99c110872/pymssql-2.4.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:89b69fd2f77f51552760d42941d04bfff79ff51211a6313de6a25c8d5aa31685", size = 4221768, upload-time = "2026-10-10T04:09:23.92Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/13e0202acdc769fc8a8baddf564c4cf1ee0019ef31dc4455afbb8c614683/pymssql-2.4.4-cp315-cp315-win_amd64.whl", hash = "sha256:b44e4f781eac5119061eb4478e6025dc05da98db029df185522eb3c38a5cdf2f", size = 2374420, upload-time = "2026-10-10T04:09:25.333Z" }, - { url = "https://files.pythonhosted.org/packages/2f/19/bedbcbb98ac6624fd21f8dd59ef9f69de2a07402cb8132a28d362bdb78f4/pymssql-2.4.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:db8c0d04afa884b57e19d45b025b10767ee7aaa5e5ed9758e536062319822cb9", size = 3222833, upload-time = "2026-10-10T04:09:27.035Z" }, - { url = "https://files.pythonhosted.org/packages/56/44/1183858e50511d1088029ef1c0a2fe59efce40abe3d5e69f7eb0670dfe73/pymssql-2.4.4-cp315-cp315t-macosx_15_0_x86_64.whl", hash = "sha256:8541a520f0355e31d7b1ca30829dc383ce1ab4530dc6513d8fad97e791aba07f", size = 3015114, upload-time = "2026-10-10T04:09:28.56Z" }, - { url = "https://files.pythonhosted.org/packages/49/22/22b070d1d254b62ddfece4c789a713d7dfddf3885275c8bda38dc50d66ad/pymssql-2.4.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6606a68ba05f467d7d4ed6e979a962eca84fe57adb74957acb1ed62953387d1f", size = 3088943, upload-time = "2026-10-10T04:09:30.58Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d2/42a5fa47a8cf077b7755cb873dc8c52accc83901b4828dfa3838032ccb42/pymssql-2.4.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eafb731d8fdec7800fa738c4b88062eb712c742caa87acc953654e5a9ab7484f", size = 3268608, upload-time = "2026-10-10T04:09:32.072Z" }, - { url = "https://files.pythonhosted.org/packages/cc/39/f7c339842ad568cf7a12ba4ff3d317f402c1d31d9896a6e70f27a163fca7/pymssql-2.4.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6651063f5ec2ab7be80ccaff7442de17e32ad6d7cc9b6cbda66b9b0ba8b3bc90", size = 4481171, upload-time = "2026-10-10T04:09:33.749Z" }, - { url = "https://files.pythonhosted.org/packages/32/f3/ec46b9c5721d3292621c1b8f49b61a45da7fc032cffa5e97d022bcaef486/pymssql-2.4.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51bb4f577da737a06532d8e3292d578a5aaec942c1461e05735c2b57471c91c5", size = 4218909, upload-time = "2026-10-10T04:09:35.289Z" }, - { url = "https://files.pythonhosted.org/packages/76/7c/d566b6101d4a22bb953e61489f6e9476c2fafa828ae4d9c2afc9ecf7e9d9/pymssql-2.4.4-cp315-cp315t-win_amd64.whl", hash = "sha256:54bccdd09ff7714b691cb06c9b9d121424ad0338e20ab9ca027814765033174a", size = 2390318, upload-time = "2026-10-10T04:09:36.749Z" }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/86/ec7658cf5ff34dbe6f9e631db252b6f22bd39129af105c489f64754836c1/pymssql-2.4.6.tar.gz", hash = "sha256:2399f7dabcbacd56f10337f353a885a0954be611f6ec8da0e9a5d0f6f0311e0f", size = 213011, upload-time = "2026-10-27T02:53:50.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/79/47ed1789fcf03a7fd2262f2f6d4a1425c73bcc5b5451ece7c7047a805b9f/pymssql-2.4.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7e3c4c2b7052c65cfa619491ac870828e0efa6eead1a086cd36f043da25e3634", size = 3244488, upload-time = "2026-10-27T02:52:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/60ace5e7049e68b23f678fbcef5a3d190603918277c84ab9c8599ca8e138/pymssql-2.4.6-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:a739b6087ccb10a3580f5eb464410437f9962acd635d0e45df65b80ef08ece9d", size = 3017947, upload-time = "2026-10-27T02:52:25.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/71e2bd433b642d6c04207db235efe70b8085d7fc438a1e2750bf964a382e/pymssql-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ea0bdfa45b7acd57c15c4184435f7595810ec01190557277c210eb1c8dfd69a", size = 3071283, upload-time = "2026-10-27T02:52:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a5/de6c8f98f1c46329da2b5b917348ebd725e5d7593d7c62c1097da6dc3b55/pymssql-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3369b93e35d99cbdb428dd6a237b407abe825c889a7d983494e778c906d474b8", size = 3249185, upload-time = "2026-10-27T02:52:28.733Z" }, + { url = "https://files.pythonhosted.org/packages/dc/21/1e80a7996aafca4e163bbefee4c45829e307a31a1bdc51c5518eec1e9fe2/pymssql-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f7e052230137112b92b5b37282c5ad7caa9cef7af286c5db3876cb4539d4454", size = 4462767, upload-time = "2026-10-27T02:52:30.054Z" }, + { url = "https://files.pythonhosted.org/packages/66/fd/b1feeba236282673b286556c855f0d6ecb18ecc9f0b0f97da8c62fa5e7d0/pymssql-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a0109c9be7ef18adb27252b72227b420f3cef03db4d492417be156bfced38080", size = 4199943, upload-time = "2026-10-27T02:52:31.192Z" }, + { url = "https://files.pythonhosted.org/packages/ea/15/d59efcbf072b4f93bdf2b47920ed08bbb5b5aca69dd7e947b520e5f84266/pymssql-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:76fccef34fced00b5b9bbe5e30bc6735f13add5a490f10228c2da804a62b7455", size = 2328615, upload-time = "2026-10-27T02:52:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/6415d161fdde234b4f065b61505234e7556b07d34edf12a6781a3066b8e1/pymssql-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:b2463bd5840977eaa18a1fd4f353defbe7cb5b6026ac497aac30d5506565fa57", size = 1855993, upload-time = "2026-10-27T02:52:33.836Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ee/0f7da0b75041f3700839499a7b579688d5569f2e93fbf88712addf4917d0/pymssql-2.4.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:c5d8ccef171b335e6ed3da4689f113a5cdbcce738ea5f1870cc7c71f763a0f8d", size = 3230920, upload-time = "2026-10-27T02:52:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/4e/69/20fa7c64b5f826919381e47af5e4539f1581d592707bf43b3bf81c81f6ad/pymssql-2.4.6-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:ed10c66ed9c1ec2964480c0ab2daeb4f6b6a6ee261a0e42abe45a74465cdad67", size = 2998868, upload-time = "2026-10-27T02:52:36.256Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/d0cb02444abd419268a226769d66398834ea635e8443508d736a4bf03a15/pymssql-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6ea24c9983543db3dd33c5806ce0425d741a3e2104090e8962c9901803ef653", size = 3102143, upload-time = "2026-10-27T02:52:37.773Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/5c561c35a0be1e234f16658b9a76e5adacbe0a77c91d93390ba5ab3e5ca5/pymssql-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd1e5a1af9508033ef14bb8d074cfbf6cf2a4e04aab09207e63c9d4b37e3c485", size = 3278701, upload-time = "2026-10-27T02:52:38.988Z" }, + { url = "https://files.pythonhosted.org/packages/40/55/c9c6f537863399437d22a13bc3472da82638dcde9ac554ee99fb0545593e/pymssql-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31e9a10196b39d9b0645009b122dc1e18458b84e93e3789febd9f3720e11054", size = 4492333, upload-time = "2026-10-27T02:52:40.49Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/d3870696da44fb126fdd473f890c048ae4f2cf6e46df5abebb9fe4c8d3a4/pymssql-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afee0c02cf275cda2b7498e1019a938a00e8765b08cec71634a1fd45757b900e", size = 4228007, upload-time = "2026-10-27T02:52:42.229Z" }, + { url = "https://files.pythonhosted.org/packages/bc/70/cce95b45f6f0d214ebc1a368e7088ac86e86cae638e51210bbb03f3c92dd/pymssql-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:f1b1d1a67a447e995f9588018170fe3c940f74dd8703b829edaf7dc332debd32", size = 2316243, upload-time = "2026-10-27T02:52:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/31/b2/1b4556be296c5fd2aafaec2f2df53479c22c718db2ce9e956c983fddb0a7/pymssql-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:786eedbef791a9faf445a83020826254f7d12fcf2cd453328399867de1fa162a", size = 1839928, upload-time = "2026-10-27T02:52:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/76/710e8cd697871ea43da3cafac4fc7e86ee3da9ba3d80063947ca8565adf0/pymssql-2.4.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ce1b67bce86d0572f7be8e5e63520384128b3372d33f68995415aa46f08b7ef0", size = 3235014, upload-time = "2026-10-27T02:52:46.565Z" }, + { url = "https://files.pythonhosted.org/packages/88/20/76c381d2edfa87b2a83e64b93891a0fa496f521727a85961b9218d6e17d8/pymssql-2.4.6-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:58bc76bfa786042ed82ccb66f9032cfc3c28ebb99ce5c366b93b232577c2adb2", size = 3004184, upload-time = "2026-10-27T02:52:47.984Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/6ea8b768c9fa4050c7a5f4deb52076a8f7042cb422279e7b8f1dfd238ef7/pymssql-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aef754ebb8c08ddfde02e67f5af04a186bccd10351ab772de8cefa75df68dcbb", size = 3101530, upload-time = "2026-10-27T02:52:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/4d/25/1abe10b22255df537786227a3e3753775b0af77a858d499ab3a40b5f1f7d/pymssql-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfb91d65576ac7a7df91c62d976357827dadd9894fd4ba62d78f8cc040a5540", size = 3278294, upload-time = "2026-10-27T02:52:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/a9/61/3f50631191d73515921eba30daaef8e2ea64a7c5d286ca5f03ee9cf108f5/pymssql-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e2f8782c4320ffc28bd3abdb14b9bf968eea26f3ebfba1397023d9f3dec20549", size = 4491947, upload-time = "2026-10-27T02:52:51.773Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/faeb38fa0d11adaf6ee80e818d34059d62b3ce326500521bbd3f9ef8c238/pymssql-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44bd7fdacba391c9938588c24f9190c083a04ce47fa21cb32531dad6dc303a00", size = 4227600, upload-time = "2026-10-27T02:52:53.025Z" }, + { url = "https://files.pythonhosted.org/packages/4e/35/556c317f18f36ae714efffc94dc972e003088158890e5deb3e1c908d3f6f/pymssql-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:e4ff7d8c1adbbace3591c021149ddb8fcd82697eb2cf75dfab627b8df846b06a", size = 2315770, upload-time = "2026-10-27T02:52:54.717Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/94638bb0ee7064250abe0d7f148e74cd8c759f4108ffbd663a189814616b/pymssql-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a9d66b0c6a4c0210c3e1b6c2054be71e0b85f2dc673eda27602fe6856a401c20", size = 1839188, upload-time = "2026-10-27T02:52:56.139Z" }, + { url = "https://files.pythonhosted.org/packages/22/4e/d45bed050927a35e8e96fa3068517cb6de001c80892e6766bdd3e6b1902e/pymssql-2.4.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:7b33b31e1f0e141d98c38e2a7c028ce8d8edaab23418bbd8558240426f73fa35", size = 3239406, upload-time = "2026-10-27T02:52:57.471Z" }, + { url = "https://files.pythonhosted.org/packages/75/67/7a7664711669de9195fc4b91b87a1853f8617e8e6788df0570869da4dbb6/pymssql-2.4.6-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:258332d757744772a467ea982a2c9cb79c27ab7d93e346ffb39c34a9c5044aba", size = 3006181, upload-time = "2026-10-27T02:52:58.725Z" }, + { url = "https://files.pythonhosted.org/packages/93/be/00e73d5e9f651e949b23225c082378e434bf03923863cfddbeb726a1f341/pymssql-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:277c5a1262949a51a66bdfa62da86b2b81d11186ae4e7ecf7b1c627931262a48", size = 3099917, upload-time = "2026-10-27T02:53:00.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/f3/fbfc3dc49a717d753532148504c56f74c2bb630733e6e25eec8be6a604ad/pymssql-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a195e7b84ecb9436e5f49e4ad26dd0dd2ed4775512e649762c29fb05ea13da3", size = 3276344, upload-time = "2026-10-27T02:53:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a4/bd7226ca9a24fbf5118493391f6e6bc20994650b07d6bead6282fd21896a/pymssql-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:65d3e2ca8b6aa56cb45b216e7db420d43b7b8a7fa155eeffe3eccfbd61d86777", size = 4490503, upload-time = "2026-10-27T02:53:02.784Z" }, + { url = "https://files.pythonhosted.org/packages/02/44/75ede4d8f7fad21d409d497926a160536a44c91994a34bb967459dc12a3b/pymssql-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7177bc90667a9d89bafbd9ccc44b5db20baea47f4b0737556b501aa489787f74", size = 4225591, upload-time = "2026-10-27T02:53:04.02Z" }, + { url = "https://files.pythonhosted.org/packages/d7/08/e55095d93eeed2ea3a3d90375d1dba48d19f0bc4f096121d2b98055aa723/pymssql-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:4a3b54b881edc5f051f17fd897a1617ec2572f194ab224007a17495c8d03f1ae", size = 2378302, upload-time = "2026-10-27T02:53:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/55/30/b470d11026bebbdd843f9bb55cadce739d72afaa16749b3793e1a4ebfbb6/pymssql-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:662d2044af8649df6357723dcf5e85a16a662ccb2669427f4a5cee5b18366193", size = 1908943, upload-time = "2026-10-27T02:53:06.867Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1a/c3e4b5366e5c1e66a1cf357a795d2eb98134ab81144e9f0a83b1dae659db/pymssql-2.4.6-cp314-cp314t-macosx_15_0_arm64.whl", hash = "sha256:555c2804c85a4f93b07ddd77a545d920f7a194eefebd8860acdd83f1f9a7818f", size = 3259390, upload-time = "2026-10-27T02:53:07.999Z" }, + { url = "https://files.pythonhosted.org/packages/f7/37/3547af6506a3eef3415cc726e0fdddc26ff04afbe8b3862eb9c5059a10fb/pymssql-2.4.6-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:5773c7994b54ba0e55fdac1cd4f8ff779d9fcf0fc664a4a28b7adec6980af98a", size = 3020987, upload-time = "2026-10-27T02:53:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/fecac21e4b815fb68317bc30d397412958eb5a935353670ddc20c920b579/pymssql-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:335c906adaf0ebc9dc0583cddf818304f5b9efc183f3b342845b86ca8428358a", size = 3093843, upload-time = "2026-10-27T02:53:10.87Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d5/fb461ab7a61c9447f3b3f6b0d35cc09aeecc9bbbf8b1b19c5f36df63dba2/pymssql-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5a07b13f200d5d94a489bbca5ba0af27de48e529e0a08447d339300d9c3ecde", size = 3272634, upload-time = "2026-10-27T02:53:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/96/22693e25c0f10cca54e4b13891b60fdf983b2c974e5eba237c2f394a0498/pymssql-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e18373991d65852f49a61141ed9c37d955bf9c923e7ff672f32696760d3c07fe", size = 4485517, upload-time = "2026-10-27T02:53:13.776Z" }, + { url = "https://files.pythonhosted.org/packages/66/02/fba1cae31e32cc6a08a6c7b0d87f9bbce53274ec9a36ffd0162b3a39bc5f/pymssql-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b9c16b49e60c5f24814331943013df9b10fd0e377266399b03c8238ea418c38", size = 4223855, upload-time = "2026-10-27T02:53:15.462Z" }, + { url = "https://files.pythonhosted.org/packages/29/df/85503393fb196a00b85eb3188522a7ddb94ae99da9d665d35083f4cfc663/pymssql-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:5efc2c9d6ffe6e922140db513bf0094b6b10d6a04e259fd3b9203c5a032048d7", size = 2397136, upload-time = "2026-10-27T02:53:16.781Z" }, + { url = "https://files.pythonhosted.org/packages/55/84/fd08b664b5152adb72de493613408be37447a3a7badbd7c732b26ee5b331/pymssql-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:29f8a8a1f02928262549c6bc9f35c58bb42197829bdc0fd3a107cc9a9906f727", size = 1923884, upload-time = "2026-10-27T02:53:18.044Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/cfc01cb4e58b99abbebc65cdfd5c0cda81d6277217ce7bde83af4e718448/pymssql-2.4.6-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:10888784ef1d8b8ceeef48db3fd7624c45b5daf0053c1a8c9efac29ea322b65c", size = 3237778, upload-time = "2026-10-27T02:53:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/d77d92560215192c6f36ddbf4abf6d6ef0d150a8a6907b3fdf6860538532/pymssql-2.4.6-cp315-cp315-macosx_15_0_x86_64.whl", hash = "sha256:6bd1bd6b15ebac0b990fb54241cb98c0c438f7d4c8d455a5d023789a3e4c2a58", size = 3004893, upload-time = "2026-10-27T02:53:20.585Z" }, + { url = "https://files.pythonhosted.org/packages/5a/59/275d30d325e7c1b12c48802b8407f3105bcda87d493f447d480f4f1ac105/pymssql-2.4.6-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6688e398ff56713e4fa7e662b1e2d4706fe27338622c963cfb779d84753b228", size = 3100246, upload-time = "2026-10-27T02:53:21.814Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cd/01336858c679a5e0ad6ecd14025fa04c30392d57d966613b0c9d1c438dbe/pymssql-2.4.6-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:808cc5094153f326c96d1bf392baeee82cc98a24a65a556799be40645d81d473", size = 3276460, upload-time = "2026-10-27T02:53:23.378Z" }, + { url = "https://files.pythonhosted.org/packages/b1/37/9a39a7b8ebba910c6541d0e8b44fc5fc35422321af248000fd733f7732b1/pymssql-2.4.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:216f754182a223b37c284925b56cc4dd4b14f9b9249bd67dad0ae476e5c80c0d", size = 4490837, upload-time = "2026-10-27T02:53:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b5/c3687a05791b05753255df0a84d292cc0cce50c751e3f10de30b22992c9d/pymssql-2.4.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9424d018537bda58f28d57ecee955b3becc7617bbcce94108c4578a44093c4a1", size = 4225821, upload-time = "2026-10-27T02:53:26.083Z" }, + { url = "https://files.pythonhosted.org/packages/d3/31/7d7e2265c7ad600e27afcf396551ba5710571a49b149d9afa793a85a1ed9/pymssql-2.4.6-cp315-cp315-win_amd64.whl", hash = "sha256:ebceea523fa483a2a13db888f282d609e557c6766c8ed90519d044a1bce6cd56", size = 2378073, upload-time = "2026-10-27T02:53:27.403Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e3/7eb949d0a80a50570d926251644cab52bf4a8f37e8a48aa9e27e3a49c7d0/pymssql-2.4.6-cp315-cp315-win_arm64.whl", hash = "sha256:1bf080fa58d5bda9b963fdce8d62bd76f234e8d45f716895ea51f794bb161284", size = 1908506, upload-time = "2026-10-27T02:53:28.633Z" }, + { url = "https://files.pythonhosted.org/packages/10/82/7227c27bb701ea6ac258a9b7894089dd3d6aab5c5df27254d42409bb4b8e/pymssql-2.4.6-cp315-cp315t-macosx_15_0_arm64.whl", hash = "sha256:e5b1126ee2fa39816aec275868c9ef2fa2dcde04156c0e77629c33c782f7a37a", size = 3258074, upload-time = "2026-10-27T02:53:29.892Z" }, + { url = "https://files.pythonhosted.org/packages/b7/28/d3354a43646884ec6f20cb89b123fc04da781f943ebb0fbb5e81e1082751/pymssql-2.4.6-cp315-cp315t-macosx_15_0_x86_64.whl", hash = "sha256:adc59a5c3ba6520ee52c6290c7c91797423ddfdf46336c1a470b209c9f97fa2e", size = 3020095, upload-time = "2026-10-27T02:53:31.264Z" }, + { url = "https://files.pythonhosted.org/packages/ec/67/696b8e7765b8a881fb08d36aae489cd6c1ce628ad430102342848ead4abe/pymssql-2.4.6-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7473ff8756717ae869e49ec7aba2156ee480ed91369109f69b27e06da80c4ff0", size = 3093373, upload-time = "2026-10-27T02:53:32.466Z" }, + { url = "https://files.pythonhosted.org/packages/31/08/62596da0ac775d4a209793898d1896a2cfebe68bc1fc65cb09c47d64a64e/pymssql-2.4.6-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:465e9e3227e5f96f47cf30fa974ecf3371178e07caa5ed8ae9cc3302c4dd93bc", size = 3272143, upload-time = "2026-10-27T02:53:33.779Z" }, + { url = "https://files.pythonhosted.org/packages/f5/41/67c5827e9e6c83554001aa4fd682278703bdfa9da4ed0440ec9cad102f45/pymssql-2.4.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d04cf86a39f3c49904a156a01650a2a62348585fb88134980cbd2a458b263dc2", size = 4484746, upload-time = "2026-10-27T02:53:35.223Z" }, + { url = "https://files.pythonhosted.org/packages/a2/68/e2da61131c47605485b7c85976ee4be3b434a8e07ffdd2c00f2f0f47c17e/pymssql-2.4.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:d8d34d384cd5a5dbb78084ed6473f9a4b0f9eb99b4291cb53cf2eb830156fcc5", size = 4223242, upload-time = "2026-10-27T02:53:36.603Z" }, + { url = "https://files.pythonhosted.org/packages/17/f5/387ff4ace0320f2ee8f3e262e48c65a0fd4186bf889fd86f9d5a33b894d1/pymssql-2.4.6-cp315-cp315t-win_amd64.whl", hash = "sha256:9b9d195f4deb772bb012d35d6b79e365d7209d23e68801bb8b64449948ae3614", size = 2394827, upload-time = "2026-10-27T02:53:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/10/c4/54c9d83f609f57f305866ed29cf5225f69ebc3900c0dc1ed4177b9c36345/pymssql-2.4.6-cp315-cp315t-win_arm64.whl", hash = "sha256:6e8cf9bffefd3a211c7dd555bd1a33c24ac2050b80a83e6a4e845b2c838444e0", size = 1921745, upload-time = "2026-10-27T02:53:39.099Z" }, ] [[package]] @@ -5043,7 +5031,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -5052,9 +5040,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] @@ -5062,7 +5050,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -5074,7 +5062,7 @@ wheels = [ [[package]] name = "pytest-databases" version = "0.19.0" -source = { git = "https://github.com/litestar-org/pytest-databases.git?branch=test%2Fclientless-all#735e1527bf38b9208495931c908d66f01ccb8e98" } +source = { git = "https://github.com/litestar-org/pytest-databases.git?branch=test%2Fclientless-all#9f6fc17c5703f618b5895fa43ce62e736e44c28b" } dependencies = [ { name = "docker" }, { name = "filelock" }, @@ -5163,11 +5151,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.5" +version = "2026.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/21/d83d6ef28c4c912c4bb4d1dcf591f7b8c6bde87b9c66f9f454677314e16d/pytz-2026.5.tar.gz", hash = "sha256:fa23724b9c486543b9ff54a327ee7569ac83ade54bb9afd0fc18676620401c86", size = 318572, upload-time = "2026-10-04T02:37:58.719Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/b0/168030b0bfcc334cf09fa3313afdb888d892c8a0027324683d1de0ec277e/pytz-2026.6.tar.gz", hash = "sha256:3b0fc2727707663d6e517936f444fd8a8b240f1fc32f4bf7ee03274245945284", size = 318520, upload-time = "2026-10-29T09:05:47.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/ef/c66110d46fb800dda0bf33164182dfadabe26a90e4476844d502a23dca8e/pytz-2026.5-py2.py3-none-any.whl", hash = "sha256:e658af3757f9e26a9d25dd2aff38335acd92bc9104f890a894b2c1ba28311b03", size = 506342, upload-time = "2026-10-04T02:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/b6/25/564f3fc5b70c1398d706c51f2b1470d0ea0b1b2f96fee67877b6f5bdf3b6/pytz-2026.6-py2.py3-none-any.whl", hash = "sha256:7fed3fee8fd6bd4a2b7b75a8d56021d0624127bf69bd5691f115db929ac570b3", size = 506321, upload-time = "2026-10-29T09:05:45.14Z" }, ] [[package]] @@ -5247,6 +5235,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "questionary" version = "2.1.1" @@ -5328,15 +5328,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/51/b2b91aec091e86bc961e5164f00207d89f133cc82b5ddad541d2132ea819/rich_click-1.9.9-py3-none-any.whl", hash = "sha256:365e7a9d0adb42e41ea832a0a12e02c44c079a26536dee688125eb9814f97274", size = 71910, upload-time = "2026-09-05T19:20:13.587Z" }, ] -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - [[package]] name = "rpds-py" version = "2026.9.1" @@ -5474,27 +5465,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, - { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, - { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, - { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, - { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, ] [[package]] @@ -5556,24 +5547,11 @@ wheels = [ [[package]] name = "setuptools" -version = "84.0.0" +version = "84.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b3/c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1/setuptools-84.2.1.tar.gz", hash = "sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc", size = 1230289, upload-time = "2026-10-20T19:01:37.742Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, -] - -[[package]] -name = "shibuya" -version = "2026.7.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments-styles" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/d2/8017988e02791162b286576116fedd200ed33e8d8af3579ab3587df3d3d0/shibuya-2026.7.12.tar.gz", hash = "sha256:9496606f5b95595511ad86b773d74b4b8d694a4dcc2f848e6ec34e2ad71d9060", size = 86545, upload-time = "2026-07-11T15:41:16.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/70/9ca2b8187666be1bcfa82b938107247159f5b566a22b5e43be27df45039d/shibuya-2026.7.12-py3-none-any.whl", hash = "sha256:0a2c0c109e7cc47fc650b01feb671d267c0ffe2e39c98f3529e32d723a9c9d9b", size = 104092, upload-time = "2026-07-11T15:41:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", hash = "sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", size = 876263, upload-time = "2026-10-20T19:01:35.678Z" }, ] [[package]] @@ -5603,292 +5581,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, -] - -[[package]] -name = "sphinx" -version = "9.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, -] - -[[package]] -name = "sphinx-autobuild" -version = "2025.8.25" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "sphinx" }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "watchfiles" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/6a/c0360b115c81d449b3b73bf74b64ca773464d5c7b1b77bda87c5e874853b/sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca", size = 20869, upload-time = "2026-01-02T15:23:45.194Z" }, -] - -[[package]] -name = "sphinx-click" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "docutils" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/ed/a9767cd1b8b7fbdf260a89d5c8c86e20e3536b9878579e5ab7965a291e55/sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c", size = 27035, upload-time = "2025-12-04T19:33:05.437Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/bd/cb244695f67f77b0a36200ce1670fc42a6fe2770847e870daab99cc2b177/sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7", size = 8939, upload-time = "2025-12-04T19:33:04.037Z" }, -] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, -] - -[[package]] -name = "sphinx-design" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, -] - -[[package]] -name = "sphinx-iconify" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/4e/498964c2a6025e489b255015d3b5644aa93eb1dd1035324de18179af8abc/sphinx_iconify-0.3.0.tar.gz", hash = "sha256:7824ca694472d6babbe811e72cdcaf52b6e0ff4240e8cfda721f84f867605611", size = 3701, upload-time = "2025-12-18T05:19:03.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/6f/7dd3097d8ce08109cff3e63a771ca1ad795d8d63d8015eaa6fbd13f784fa/sphinx_iconify-0.3.0-py3-none-any.whl", hash = "sha256:b38ca4a66767c317bea31850a6f540ef79db4394eb4792961918c2d44b4a2c3a", size = 4068, upload-time = "2025-12-18T05:19:01.892Z" }, -] - -[[package]] -name = "sphinx-paramlinks" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363, upload-time = "2023-08-11T16:09:28.604Z" } - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/87/06537dff20aa57df2d0f2afa87edff4afa85dec6f57f03f7acc24f83ff88/sphinxcontrib_mermaid-2.1.1.tar.gz", hash = "sha256:f8d7f1b2f0ad69b7ebdd5cbf532fb4ee107893b5373bc2d0e9634089c0b2cfee", size = 22353, upload-time = "2026-09-01T16:10:23.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/6a/5bf2222b8084c7a7bc3ec23d470c91e3e03e82e4ce0bbacaa61f456ce7c1/sphinxcontrib_mermaid-2.1.1-py3-none-any.whl", hash = "sha256:3b9ac1a8e0b5154dec3c0acf9b5c54dd9ba58540e416f20858c300814c25648f", size = 16368, upload-time = "2026-09-01T16:10:22.377Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - [[package]] name = "sqlalchemy" -version = "2.1.5" +version = "2.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/bb/28e3106ee586e5b2f5ecf211eec1348055f0f926314b13fe23b9cffa9039/sqlalchemy-2.1.5.tar.gz", hash = "sha256:0df76cf1ca680ad745bd378a6299a058611a8a784eeaeb4ed78ad4a0b7ce2003", size = 10569593, upload-time = "2026-10-16T20:52:20.57Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/78/d4a0f4e7432f186d3082e5f1b8573b6776dd15bda01ea2149f540c7bceda/sqlalchemy-2.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:116cad2685f16398c3486e30832e07197a7e12147e465a44a187c250059f23d0", size = 2468654, upload-time = "2026-10-16T22:45:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/34/1d/628915299f551b0a4e2e2492975e39cf2688288fc9ebc4ac8c1483d89bd0/sqlalchemy-2.1.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bac60969b1a96f8ee4db82c647a3370860f76b2a41dd7b95f578a8421c7c87bb", size = 4633894, upload-time = "2026-10-16T22:34:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/92/31/78d92c64da26adc3febcf98722426c1090be70b3ac54ff1573a5afa36aca/sqlalchemy-2.1.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:926c61c589c3bebb6c540ba0b60fd9a2e8eaf6b5ae9e3ec94cb2cbb2fe10a022", size = 4656153, upload-time = "2026-10-16T22:40:49.829Z" }, - { url = "https://files.pythonhosted.org/packages/50/57/a254f76935dfd5d15ce9e3b3cd306b26670bd9473fee1dcd80ad816790d9/sqlalchemy-2.1.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46a50f5d226e9321970867c91252a71485301b67eaee2f76db92b2f6bea5fdcb", size = 4386171, upload-time = "2026-10-16T22:52:43.185Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d7/34c21248357dcbb68122d79d68126824d15a70448c5e7719266789020160/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb7afa85837f56cd07235dbb05858f5137d5fe7a1e813b12669a52462a1b6ae5", size = 4571827, upload-time = "2026-10-16T22:34:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/4e/11/18ee59f913c99fa5c991d621faef6f1612bb70260af98d856b80e680170a/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ad305d5fed915340464441dfed47086303936288ed0a5ebc1a0b2ed41678fa41", size = 4383076, upload-time = "2026-10-16T22:52:45.106Z" }, - { url = "https://files.pythonhosted.org/packages/5a/aa/f45c4f56004a2596576cf02744adc9074ac945e98e49946551e07c57afac/sqlalchemy-2.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ca1b380f940fbd94c1df94d7b502b9c496fe9a62f5200e0c6feaa8db7175401", size = 4626378, upload-time = "2026-10-16T22:40:51.987Z" }, - { url = "https://files.pythonhosted.org/packages/53/b2/842b0416ffa5d0e0ce5c25a7b52ac31d4b1bd55e7351cfd9e7b0e32273a7/sqlalchemy-2.1.5-cp311-cp311-win32.whl", hash = "sha256:4c4666b9884e4a807b27835ab236056278915ea9549a2405e83d86297e7ba0da", size = 2384833, upload-time = "2026-10-16T22:33:38.287Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a8/6efc290e861ce6fd480e5483ba0bb5af15441ca02617e09aae648b333049/sqlalchemy-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:9edda6c6e22113702d9603b1178f1ab16f5120b49995134b407f11ebb4446b91", size = 2435596, upload-time = "2026-10-16T22:33:39.743Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/0e4241d71dc7b99aceeac0a841ae41aad24e3ede04eeed2d3c27df06cbd4/sqlalchemy-2.1.5-cp311-cp311-win_arm64.whl", hash = "sha256:29f47da90629119646ef90c52f6185131a10b6ed811b26490b01c31a9e9b2453", size = 2400190, upload-time = "2026-10-16T22:27:32.459Z" }, - { url = "https://files.pythonhosted.org/packages/35/a5/286dc0a04b2f2052b15785784db16c39628d3d7424b25112a306bd0e1d43/sqlalchemy-2.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3eb1907fc67aacc160276e7f61c1f71a7a38c10e84ff87b74d90cae0abd137bb", size = 2471746, upload-time = "2026-10-16T22:45:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/44/80/066b7dca8250d8a6135afe692b8a80bc5239c0a0344213ba847fc3f9a46b/sqlalchemy-2.1.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22eeeb297ced15cd7571aca242aede6a59141dcc3282c29c987fbc83f05c823b", size = 4673470, upload-time = "2026-10-16T22:34:28.997Z" }, - { url = "https://files.pythonhosted.org/packages/4d/33/8be720a412d595fa061a9ddcff6b5340ae09d3317ba6a32207523eb239f3/sqlalchemy-2.1.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb2fc3c1e8d2498d944b6ff64d32a63a5c900a14892c200626e701e69187113", size = 4726119, upload-time = "2026-10-16T22:40:54.421Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/938538f2d7fa2e1ba62d07beaac3303c9ec0d21479e35320cbe5b0312ccc/sqlalchemy-2.1.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a534a39a9247a17843d9f6e34a0877d1a2af870b273e53e1c453b3a65f93c28e", size = 4374608, upload-time = "2026-10-16T22:52:47.037Z" }, - { url = "https://files.pythonhosted.org/packages/35/e4/b27d25f452994dc9acecb86f7984750c0f7891734fc1a50aee83483fd88e/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cc3292040c980e9f1a54b14da76cf938a6a04a27e8acf6cb7709e9c3a40935b", size = 4596779, upload-time = "2026-10-16T22:34:30.811Z" }, - { url = "https://files.pythonhosted.org/packages/53/51/f2fae2a6bb734400aec41f3e625efcc03f1e43a26b852aa89206efce61a1/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ce8d3de4e0489671fa023dea3f769360df02b2df6e482a44f8fa4421dd41c0ae", size = 4367657, upload-time = "2026-10-16T22:52:49.12Z" }, - { url = "https://files.pythonhosted.org/packages/0a/22/05957d18ac378d20ee048010916d3c1931442bcbd0c1c21b18e0e868ab6f/sqlalchemy-2.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:143cea634f03f9a8009fdeaeab46808031ab05bf0c50ec0aa25da51c29bdeeb8", size = 4673278, upload-time = "2026-10-16T22:40:56.847Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/266829855add33a99f1cb59d548e563e208aea514277d2f9650b454041dc/sqlalchemy-2.1.5-cp312-cp312-win32.whl", hash = "sha256:7133bd0ac969ba1a108508975e0c26229329cadaaa48844f0a038dafa610693b", size = 2381924, upload-time = "2026-10-16T22:33:41.207Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1b/3f59712d0a7e08bf3a075df43b0a3d89de051728d829d6836f546c533012/sqlalchemy-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:ee7267d7ffed80c370dfdd7ad1c741d0227f0e8054b83d00715fb99307bd330c", size = 2434617, upload-time = "2026-10-16T22:33:42.917Z" }, - { url = "https://files.pythonhosted.org/packages/57/f6/31e94eb5403ca04d393805fe6df5470f48aabdbfb05437e93c2a7f8e3ebd/sqlalchemy-2.1.5-cp312-cp312-win_arm64.whl", hash = "sha256:b3732a60b01560c9efee3b9ea188dc12614b2642ffce4f8235c4fbf72dd40fe8", size = 2394015, upload-time = "2026-10-16T22:27:33.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/aa/f10fc142d2db255249107ee8b21bfbabc10b90f7fadf13c3cf898acc6179/sqlalchemy-2.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc1c86db97372e6bba18a7bcf0f6150dbdd808237f0f52802ef0bec676a94642", size = 2465792, upload-time = "2026-10-16T22:42:39.038Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0d/d395e310e36b5bd39a121cd455504aa1e9111f419efaa88be6b814b4e54d/sqlalchemy-2.1.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c229afac414d691d2f69a6f4e294ac5b5fd334c49da1f4fb86ab7f2a466271d", size = 4599703, upload-time = "2026-10-16T22:55:25.351Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6d/04f4ffc5038571e712c4f88f20d7022091bafab082e85ea5b37a7e8270f1/sqlalchemy-2.1.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cf433bcb77696cc915552d7f1e8d9117a5dae5aff9fceb06a69d74b3b3c2bd1", size = 4652645, upload-time = "2026-10-16T22:59:23.273Z" }, - { url = "https://files.pythonhosted.org/packages/11/85/08bc336576755daf087e0888e44657f4f72b6676a99522e7a612f0c2481e/sqlalchemy-2.1.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:399d3e212cb557d5bd1ca6f79981e8c3f45c0f6be60b63a9ded790396a81af08", size = 4316833, upload-time = "2026-10-16T23:15:10.011Z" }, - { url = "https://files.pythonhosted.org/packages/a7/5c/476441255cc7afa25e8c75e5a61d3c5c3af7cf61c951ebed2ee9873ec4b1/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a51c56afdb880e9f83736a2e81c02dd002fe1c5ad68346a35d97a2e135b1568", size = 4523134, upload-time = "2026-10-16T22:55:27.374Z" }, - { url = "https://files.pythonhosted.org/packages/63/ca/064e5e3ca7c8f256c8f9b5206eafd410a39226c2ab6fe7e8c1e70725e432/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:17f8c68bd35bb76b2d9ef1097941fb783ef9ef6604c1d2a72c2e276430aeffca", size = 4318058, upload-time = "2026-10-16T23:15:12.139Z" }, - { url = "https://files.pythonhosted.org/packages/90/8c/e38956834896e21038a67193f91b189b9a61555b92fac42368027d5a2524/sqlalchemy-2.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:59dbd7412531da0124df8667a244b5155773632e9bd79a5c2ead393b11b59de5", size = 4613916, upload-time = "2026-10-16T22:59:25.19Z" }, - { url = "https://files.pythonhosted.org/packages/14/b3/5531eb9d10d4bd38bd7e6c505776ac73be259dc117c9f07a8c46db4e9a2b/sqlalchemy-2.1.5-cp313-cp313-win32.whl", hash = "sha256:3f566787cb3a6a25ad8c5170e90167d4e8ea103d412aa4ea95021a6df0b95964", size = 2379136, upload-time = "2026-10-16T22:48:37.111Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/c48924aafae616d59a2eb0375feeaf4bc4356327a442bda3b9205861b543/sqlalchemy-2.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb8dcd5e1cc12fdd4d490819ec7a56e8c02e86ca9087a472e28b422ff932bae0", size = 2429688, upload-time = "2026-10-16T22:48:39.038Z" }, - { url = "https://files.pythonhosted.org/packages/51/84/0855a7d42b700b091af6f817c171c2ff97478b0d5143fe61963adbff9201/sqlalchemy-2.1.5-cp313-cp313-win_arm64.whl", hash = "sha256:65eece9d9494e46de1eb3d49eb438acfcecf059e7a92a2f3daf72ec26d371757", size = 2390320, upload-time = "2026-10-16T22:31:43.871Z" }, - { url = "https://files.pythonhosted.org/packages/01/b0/59b26375cd618e861a9295d855b866ca85be654d20ed47e494e33c3bf26e/sqlalchemy-2.1.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5541a110ac4bb6377c785aea1b8f0b4d6658ed0ca5061ed1115d0725062b026b", size = 2468925, upload-time = "2026-10-16T22:42:40.547Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a1/01931ba38770f169ce70e4034b0dcf2c1a776fe7d8dde93a34b6355510b1/sqlalchemy-2.1.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f029b2f7114623c03f762596f313515a2358004c44eb18924a15940f2e27d30f", size = 4593220, upload-time = "2026-10-16T22:55:29.253Z" }, - { url = "https://files.pythonhosted.org/packages/42/9e/fa478482503a69dfb3b0cb82f3f80fe3816026530dd34b5359e2acd9f30d/sqlalchemy-2.1.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc44dcc80e89a006af415edffc7409b1f2976a4597065449c166987f95801ab4", size = 4622806, upload-time = "2026-10-16T22:59:27.153Z" }, - { url = "https://files.pythonhosted.org/packages/fc/01/357ff85423013de8316316752c36a6bd1b7f3830c30dc63754afe06ba589/sqlalchemy-2.1.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:72ddd7a8ec187d02de0ef316a15aaf381ddcc594958247b02a3dcdb4c42f0b02", size = 4316247, upload-time = "2026-10-16T23:15:14.27Z" }, - { url = "https://files.pythonhosted.org/packages/43/53/88c5af325c81c929d0ed5b7ec15b087173bd3587d93366c5e336b9647267/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f5173a6e60a15090f9e2348a8ca94cc13de72796c16733f3b62fbb22acba07b8", size = 4522522, upload-time = "2026-10-16T22:55:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/87/29/f6f01f121d5e590c568f405f78b8eb3edace446c114126fc428536446dd6/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:09b10bd68223d57cce39c08947efae3f91ac40b552e07c7c869c3350a0fc4c23", size = 4317593, upload-time = "2026-10-16T23:15:16.397Z" }, - { url = "https://files.pythonhosted.org/packages/0c/6f/14863fc9aea433645851330242823b1879cdaac71c118f7c9366e7caf4da/sqlalchemy-2.1.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ea4cb5022462d9f15e7b72a52c267385aa97ac224c8503ac63ba90c0067ad0d9", size = 4583207, upload-time = "2026-10-16T22:59:29.322Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e9/6907fc009d9d06e8d03d6bf286df46e878ea2a85c839dcb65ef11ceb3a39/sqlalchemy-2.1.5-cp314-cp314-win32.whl", hash = "sha256:c3ffbe6c680c626403b7e9697d92465501078731b9e8863b1410826e66b950e1", size = 2386018, upload-time = "2026-10-16T22:48:40.692Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d6/dea47b4b361c427d390edc10d73050e6d11d905c56ef26861ff5f94aa8fa/sqlalchemy-2.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:061d089901319463a1160e4596191e355d2a9b4cce4852dac5adc301b085ac2f", size = 2436983, upload-time = "2026-10-16T22:48:42.2Z" }, - { url = "https://files.pythonhosted.org/packages/be/28/305a91768a1ca0980c6ead455dcda5a2c1e616022901898e966ccf913ba9/sqlalchemy-2.1.5-cp314-cp314-win_arm64.whl", hash = "sha256:f5bc7fb93f1c17efefeb062bc4d2742ad1b71d56c36e6e2898a371ac6a56839a", size = 2401394, upload-time = "2026-10-16T22:31:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6d/d4bab269d94ed1017950873643b5bd3eb271a5b8d5b0451250f0173f90fc/sqlalchemy-2.1.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9bb993b1d0890a2da90589684c3d09de75df82c06aa8f7eda246c7cd0ea3e820", size = 2508315, upload-time = "2026-10-16T22:55:01.45Z" }, - { url = "https://files.pythonhosted.org/packages/d9/80/aeab72b51fceb71b1e1d62cabc6f51c9d8f20588165aa54e0d0d6cc77c05/sqlalchemy-2.1.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df7e71ed051e4003e73484ef6547aacfb3a2322aac0c230d530c97e48e77b4d7", size = 4924848, upload-time = "2026-10-16T22:41:10.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/8b/7318ca0ae828fa1605dd4481f8eeecb5325a7ead6289c1206fb1ff1e2e1d/sqlalchemy-2.1.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c76d4cb9c76a6906563133449a76d9eae6c8529ca09484b8615ca604442b442e", size = 4835557, upload-time = "2026-10-16T23:06:18.482Z" }, - { url = "https://files.pythonhosted.org/packages/35/17/7a5a196b0af72d65cbcd3a6fb7c4128693d039faf58a682ffe2d19329420/sqlalchemy-2.1.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59a56be2e4e972061f9d5c1d5b01763b8b2207b48b956fbb6879c7443f5817f7", size = 4513707, upload-time = "2026-10-16T23:28:18.969Z" }, - { url = "https://files.pythonhosted.org/packages/91/50/ca323ed15fc06c73a7fd85a3a339749994c560c12312ee8a53abf0f9a7ec/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc1a61d7f42e71746b5a2e10e90a6b174b4add441b7b6cf14278f1e029b2b800", size = 4781127, upload-time = "2026-10-16T22:41:12.784Z" }, - { url = "https://files.pythonhosted.org/packages/26/3d/c56a75d0a2f6af08f6bb28d0b69e129911a2fc52b24dbafd7a7e5ac5c1aa/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:740e96dfb99a615c8056070d3747efe8bce4c145e5953ccc9e4b004c139de0d2", size = 4513816, upload-time = "2026-10-16T23:28:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/7d/dd/84e0cd627bbd3e35b5f033191a4f597e611ff96e424db36a444a8d6755cc/sqlalchemy-2.1.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:92a0bb0595331c8e40bc615a67cc7bca9b761487409d1bf46c598b6ba5c0754d", size = 4772462, upload-time = "2026-10-16T23:06:20.876Z" }, - { url = "https://files.pythonhosted.org/packages/b7/47/cf0a920fc40344f8a1806d661e7c702341aebd871e31e1d1d6e0fbc9d1e0/sqlalchemy-2.1.5-cp314-cp314t-win32.whl", hash = "sha256:11185c72b5372369166cb721537943f8e1e6e42a399e9bac19e02f3ee20592a4", size = 2444861, upload-time = "2026-10-16T22:51:51.169Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1d/4c5dc7e30fe740a01df4ba5ab800be2d7674b87f042e4c21c7ca89b36f9d/sqlalchemy-2.1.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5173207b8fe1ddfc1d451c420a2a9cbbd0734b4a19896b8080e3744497e1c20c", size = 2512497, upload-time = "2026-10-16T22:51:53.014Z" }, - { url = "https://files.pythonhosted.org/packages/5f/2f/d3e9bfb1faabea302b98ba8c9dd6252bc362c6afeabe28ffcc1eea8a6bd2/sqlalchemy-2.1.5-cp314-cp314t-win_arm64.whl", hash = "sha256:def2f117a6ec0a25202ee36a148bbcfad3d43e2f2bb2e698e6d4448c6db5cb19", size = 2430598, upload-time = "2026-10-16T22:32:54.554Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7c/17f07378eea7ec2fac8ac60f99fe3c474ef5c19a3adede70ae9886eb82d7/sqlalchemy-2.1.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:a0b80f91d9ed92be7a134278ea2e83a693c07d9986824ca51d859af9e57e18f5", size = 2468856, upload-time = "2026-10-16T22:27:43.754Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c0/4fef29827b198937e09c40d0fa36b9738bd3914f0e5040927dfac8dfee21/sqlalchemy-2.1.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3033ddeeeb34c6c536622c07e932a20f8c9f58d6dd76e56595a76b55a25011ff", size = 4597018, upload-time = "2026-10-16T22:41:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/37645f8445ac3774e18f8dc72e0fa0ac868e0dff0e987ff3bec644f55102/sqlalchemy-2.1.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4d30723171d511ec16ac057872a38b9b395420c0aaf81b2f94dd7bf1345c5fe", size = 4632322, upload-time = "2026-10-16T22:44:53.83Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/15466cd08a6c329acc4f730dff7041e84122456fe38c518c93a07d6d82ca/sqlalchemy-2.1.5-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cefa5badc13df1c9082bacdfa435c8eb6dcb9b32717f51d09d1e160a599646f", size = 4338381, upload-time = "2026-10-16T23:01:22.714Z" }, - { url = "https://files.pythonhosted.org/packages/12/2b/a93b7c8f6886138dac91f66fccd3b42473c365408d9a4c1b69403cf206f8/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2549e503185897de1fc5ca7730d5daed0d27d0b26df4a059dcdd33ebe81b7b4b", size = 4525244, upload-time = "2026-10-16T22:42:01.631Z" }, - { url = "https://files.pythonhosted.org/packages/b3/47/d3926cacd194be073a2053a05470f20ed7da623f399e95213c1e497954fd/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:5a6871ee159a943d874012de835d56d0544ee871a99dbae05bc053da315b2c23", size = 4337223, upload-time = "2026-10-16T23:01:24.702Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a8/b15ab4712342a18fb3949c77687c24d3749e86079c9905cc2cbebbd44da3/sqlalchemy-2.1.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:51e290080415325b66a94d20f6360e310606acb832f2bd677d4fcb95728e7ce1", size = 4594562, upload-time = "2026-10-16T22:44:55.661Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ab/3b6ef6c77740d7fbf5a3eb45f95768fb809b0769bd21df81d5f8c002e5a5/sqlalchemy-2.1.5-cp315-cp315-win32.whl", hash = "sha256:c228084025a28aa038aef9b821d842508650fe12fe51f06e9cdaa4393be7812c", size = 2385770, upload-time = "2026-10-16T22:41:04.484Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c1/0b3256bc11487b2846a26d58946caea0ed395fd500951a16fb82d31a20ff/sqlalchemy-2.1.5-cp315-cp315-win_amd64.whl", hash = "sha256:bc09dd22c3fc491a2df7d16ca4167de35573f40c2e82dd4d6ddae589a73e7032", size = 2437043, upload-time = "2026-10-16T22:41:06.12Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a0/09eaa7897f6f745de16220b20c63660948a95716be90d2f405af08222e57/sqlalchemy-2.1.5-cp315-cp315-win_arm64.whl", hash = "sha256:375ef1bf78bc269a220963163d2580d5377898ed23a00bdd98a02d755421573d", size = 2401090, upload-time = "2026-10-16T22:28:58.035Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c2/4c747dcc6fe329dd07ab57233f334eca19a3ff05934e0aba4f25a0977a67/sqlalchemy-2.1.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:7af1c92255cf8d50e87a808ce41096907332acdd09f285426a52f7c300e152db", size = 2504475, upload-time = "2026-10-16T22:55:02.946Z" }, - { url = "https://files.pythonhosted.org/packages/07/f1/183446b1bc29484087ba7a6bb199a265c0559bf8ff201c1ee54bf1fe0936/sqlalchemy-2.1.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85891e0828e76a77fe912995dbc3ee949099488ba0a2ab323535d2e767446b20", size = 4902197, upload-time = "2026-10-16T22:41:14.703Z" }, - { url = "https://files.pythonhosted.org/packages/31/0b/b24baf343461eefdecc672df1ccbb14c8c64028a11625dabc97b0bb9791b/sqlalchemy-2.1.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a72e062d2d38826dd89231df692fb40f12d7d1b9d118be23bcc1cf93c6e31782", size = 4851871, upload-time = "2026-10-16T23:06:22.861Z" }, - { url = "https://files.pythonhosted.org/packages/da/27/f688b34ea3d85b38e01b7463874f1f4b91619dfed4e6cac88c27caface73/sqlalchemy-2.1.5-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b79ee943e2e12246f873345762b83dbadd1fcfc3e832eab83272d672704dbdea", size = 4500095, upload-time = "2026-10-16T23:28:23.431Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/d8e13be8d0c25ba49244a9c24579ef61d5a6b57f98e55fbb173b7923ec3a/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:e9418d87b55c469c2f1328246d4533238cdc43134c695d6b9a6b7a87da49d463", size = 4757826, upload-time = "2026-10-16T22:41:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/a0bd2ead949cc605ce3ad5d38e2fe84963b2aa7f89e8648c1bbad13a3679/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:898d86c22640fe52ffe02b4abc043a3c6754b4875837599e739be440966dffa1", size = 4499377, upload-time = "2026-10-16T23:28:25.203Z" }, - { url = "https://files.pythonhosted.org/packages/b9/dd/c1ca0713a1a7d8d6eb5864edd2d11d7a6a327aaba664645733cb53662425/sqlalchemy-2.1.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b3ad20cd035392f2b91a6466e24643990c1378beda80c9bda87a3a41e841e838", size = 4777264, upload-time = "2026-10-16T23:06:25.203Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dc/9dec72cea689f16bc8ecfcd62b4aa2796009a7cef6be85a9f70310ecf71b/sqlalchemy-2.1.5-cp315-cp315t-win32.whl", hash = "sha256:db34eefaf992c84460ab26ecf2fc2bdcc68965d6becfae8182cae0efeaa3e8f8", size = 2441666, upload-time = "2026-10-16T22:51:54.58Z" }, - { url = "https://files.pythonhosted.org/packages/dc/28/f44c4524ce8a68935bd6f23ee801165072fb8c1e477fc9855efd9eaa2360/sqlalchemy-2.1.5-cp315-cp315t-win_amd64.whl", hash = "sha256:173054351d863ddada188284e309e87a9f030132c8d534e05458971ff061aa1e", size = 2507526, upload-time = "2026-10-16T22:51:56.262Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fc/e90c91f6c2709a431f9b75e41bd31216337859411359f4a4fc2615c989e3/sqlalchemy-2.1.5-cp315-cp315t-win_arm64.whl", hash = "sha256:22c8d0873168a54f614e381559ea83d547b2cc5488b95b049bdc92667586bd5f", size = 2426204, upload-time = "2026-10-16T22:32:56.011Z" }, - { url = "https://files.pythonhosted.org/packages/de/e9/61176e4753f74ab5f6e72a56314ee654d25c31e6fc98c99060442998cccb/sqlalchemy-2.1.5-py3-none-any.whl", hash = "sha256:2f77b77b716f005b2dc3cc059d56df6a919bfb5b439e4149890a9df6c36be9e3", size = 2060214, upload-time = "2026-10-16T22:43:30.801Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/36/dc/9addbf490110b5727380059ee542ad8353bdcee45a37ba6a5e0976966ae0/sqlalchemy-2.1.6.tar.gz", hash = "sha256:499eae5c9051efe96cb9a73ec2fbde8ffa4186f102b9094ebab8fcc9d3b99f52", size = 10589700, upload-time = "2026-10-21T16:25:06.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/e1/fca57b09555d90ed6f8ca043245d5205cb2269122acdf8eeee32176711d2/sqlalchemy-2.1.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4e4a43cde0dcdff974304ddb48246d6a094e618b6ee9642827ddbfe9fbebadb", size = 2471480, upload-time = "2026-10-21T18:34:54.083Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e593c3cbb51713e6e646409a5de893c1b6432e45339c7741a6a2edfbd09b/sqlalchemy-2.1.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b6b5fdcdb6891025117f76f48d7d0f89245d66fab6519f3b856294bab317387", size = 4636734, upload-time = "2026-10-21T18:51:15.747Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/8a1e812d3937f8367b64cf0ddcd2559cf9b0619c4061ebf08ff8084b949b/sqlalchemy-2.1.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d877b2c46298825e8f32059e8d912d0d100af0e9fefe31fb2abb52192817afb", size = 4658995, upload-time = "2026-10-21T19:13:58.837Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d4/7d42a5f4efdb859fa36e85168fcc3c824197193439ff7fac2b595bc94e14/sqlalchemy-2.1.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed6596fb63c574fd3dc4d9ea58a7b9c6e7235dc3d74a0bd3791559d5a618e947", size = 4389012, upload-time = "2026-10-21T19:10:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/4e/71/6395a7d168ce7f25d2c3f9b6f51d1ca775482c8510d96bfe75fe880ab91a/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b16a8960ad9cdc923b90dbf431a4739fc81a07b53374ff74b7bdad3be31ce189", size = 4574669, upload-time = "2026-10-21T18:51:17.529Z" }, + { url = "https://files.pythonhosted.org/packages/54/b4/d9b221dd36f82d0e34413837deee25fef45cc13dc4e0483c7ef26b1c34f7/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9245bcbcc3056d440f82e43fb34f94c15361c86b62e14cd9d7880c8e0cf370f6", size = 4385919, upload-time = "2026-10-21T19:10:57.39Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b6/9a8a6f4c2e4035c0c3edc689de4e672ee1324a1fca22f7d9b775b3b87f33/sqlalchemy-2.1.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a909a8eb82ce5a2f1566ebd685a79f3cfd9e049d57e3a0d3e24d56da04913e73", size = 4629217, upload-time = "2026-10-21T19:14:00.971Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/95332d026013398e9bf6edb608305cff4070d95aeeeadec6217018d4e1e6/sqlalchemy-2.1.6-cp311-cp311-win32.whl", hash = "sha256:69eeb530ce74be5e202a98ce35aa58b0776c24e64923c4873c25fa56f8e423bc", size = 2387737, upload-time = "2026-10-21T17:08:09.582Z" }, + { url = "https://files.pythonhosted.org/packages/61/78/ed22f1e9f3597b004da38e853dd0c443908e19e74ceee623e45ed18b967f/sqlalchemy-2.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:f0f22627d6436bd120c6dde0936b12fe75498d1e2e9214ec9863be17f9b80997", size = 2438496, upload-time = "2026-10-21T17:08:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/12/88/d0ccaa6f65d4de6929eb94b1fc474f1b82175b7251242d72cf76d77823a5/sqlalchemy-2.1.6-cp311-cp311-win_arm64.whl", hash = "sha256:8f9d12a8be271c7a83ded1cb7d85921f1b7f65c655c8dae972130a190211bb57", size = 2403090, upload-time = "2026-10-21T18:48:30.486Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/33a77ef74758261e5a2206456713e43d0a72c98ef431fe68fa88ed03b428/sqlalchemy-2.1.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:80009748a5ce0c51a9947542935f9709a1c48b65c69f4a1e56d3072f99e750f2", size = 2474582, upload-time = "2026-10-21T18:34:56.139Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/45e6e2bae7c11e7dbaeb0e9205d92b1b12583b7e199eb2503a7a603f3b9f/sqlalchemy-2.1.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b5885ad1226c8eeea6aeabb4c4036e43202e1bd70cc7a50ece98bcd6ebe5566", size = 4676310, upload-time = "2026-10-21T18:51:19.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/8fbe61673b1bb9f5e98d9a9b28455d408ed8bbf6efaf0b03db13357c763d/sqlalchemy-2.1.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e4b4dcf85d7f289def0e3167f15aaad101b7744a065fb2019476edf306712c", size = 4728961, upload-time = "2026-10-21T19:14:02.762Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e4/642e3dbd106866d7f726b7c5479f9c9539f1cf010a2e7b8fe17ee8b8de43/sqlalchemy-2.1.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c314bf9026d865c2b2f67f3b853b4e2edcbe7ed46b67ef11b4a40f0af6aa17b8", size = 4377450, upload-time = "2026-10-21T19:10:58.992Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/bcaf98d9f1ac842df7287003a0db36c0f33af9bdb07f1f716d8ab8d6c709/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35348ffcbf686055ea7fd02ba4eb76060ed76761648720c63170c5c8bccd85de", size = 4599619, upload-time = "2026-10-21T18:51:20.955Z" }, + { url = "https://files.pythonhosted.org/packages/44/ac/13226c429b8a87890433fb7dabf8a8b15725e8efdb2e673fdda336bd61cf/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:eafb738fbcba39d12543730eacb65e2d5d627965959b3cc6bd6f797b2d52cc0f", size = 4370497, upload-time = "2026-10-21T19:11:00.827Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5e/d9330f73b6e39f08baed5bd6ea410fd8ee78b8bb1549d0d6a5f555a73db5/sqlalchemy-2.1.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9b197f5d2bcd61b939d2f04b77e86ef24bda266518f3e1526bf666deb20d3ddd", size = 4676118, upload-time = "2026-10-21T19:14:04.705Z" }, + { url = "https://files.pythonhosted.org/packages/62/a3/811e0702c62666d222cae8e7ece846661833896ed8a4c1081840dd49a513/sqlalchemy-2.1.6-cp312-cp312-win32.whl", hash = "sha256:991fa7c6756505e3dc5aad678db5fb0110d9816c2185bc3e2c520de46cfc8c5d", size = 2384827, upload-time = "2026-10-21T17:08:13.393Z" }, + { url = "https://files.pythonhosted.org/packages/06/ee/5609e83ebc921663b3250d053a61b355db72ad3cbbde73e7105024e8bf10/sqlalchemy-2.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ea80b33e57f918509eb91cd074324bff0f9aae40d238121cdc369f449c20649", size = 2437524, upload-time = "2026-10-21T17:08:15.047Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/5b3164be28000eecb4629c07522686ea0fa2a340ebf529801fc861518469/sqlalchemy-2.1.6-cp312-cp312-win_arm64.whl", hash = "sha256:84b359a458611d40061f536b1363733b0af7a71425e4b17d0abb7e543d00e979", size = 2396919, upload-time = "2026-10-21T18:48:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/d7/db/ecf1cf1f5c8cb7661081dcb0561633c174213d7af5fccc17ab48cbe9812a/sqlalchemy-2.1.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0534d3f41295a74d9fe2e0838e06ce6329a0855b2965df5f5d1c4d3f6f1406d0", size = 2468626, upload-time = "2026-10-21T18:49:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/9b/98/1cccefb4fc94f4efd621604063acbbea4e3d530c27151fd7249d68adabf2/sqlalchemy-2.1.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:650f97996a4c529f4930969f3ba11bcdb57ff160ed1cb87ea0174393bf4f5f73", size = 4602545, upload-time = "2026-10-21T18:54:18.766Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/1817d7406ed92c8647a1d1db965a1bc2dd5a3f80d18e4d28ad62daf9bd72/sqlalchemy-2.1.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bada6bd29ae58a0581bf6977c0a43dfd67388fd09129f1475c37eca370cccd8", size = 4655491, upload-time = "2026-10-21T19:05:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/41/5c/0bdaa1676fb182b93e3c87c09cafd08a734ef29820aac6312b87a88f8537/sqlalchemy-2.1.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:96f5fcdfb7e89b6d895e2ddc165f67b3126a585dfdf2a95b0c1f3fcf3380d7d4", size = 4319667, upload-time = "2026-10-21T19:23:16.637Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d7/319e5a049b6f5235d1229858972abb05b8eae2fdd6639dc7bdbbc9cdb68d/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:015bd7b0687b5feaecb9988bc2f36ecb39daca83bd806b094b43b19ea503b0a9", size = 4525975, upload-time = "2026-10-21T18:54:21.553Z" }, + { url = "https://files.pythonhosted.org/packages/19/3b/6e8e5c14a3bcfae59a081a27f33e4f225df8140daab8e90fc3c42afd0d0e/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bf883ae772971bc6155cb64cdfb9e31c50a2099f3b7c8b51b0401bf608a9fe8e", size = 4320900, upload-time = "2026-10-21T19:23:18.835Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a7/7ad220d5ea6308a9a32395785f4ce8beff4d00b0ae56ab9f2dafcca28743/sqlalchemy-2.1.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ff897d13de613768123dee68474956a51fe641c42bba7af6cb7776716a8dbcb", size = 4616756, upload-time = "2026-10-21T19:05:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/08/81/53a198f5a29f247adc68c0c6da69bd2c7bdd89d3ffc58f9e626ee3160f18/sqlalchemy-2.1.6-cp313-cp313-win32.whl", hash = "sha256:9db8bacb2a69b4b5388bc1439463170af28f4cc710933085b81d1658d931d2c2", size = 2382042, upload-time = "2026-10-21T17:11:33.968Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/69e86a23cd9ccef0d654c09dc69dfea6d62b1311d2f13ca3daab1bcd922e/sqlalchemy-2.1.6-cp313-cp313-win_amd64.whl", hash = "sha256:b9f0b92b4a88b630f1e67df816b43cb017bb583479917dd100541d4f490a9517", size = 2432597, upload-time = "2026-10-21T17:11:35.514Z" }, + { url = "https://files.pythonhosted.org/packages/98/90/a7f81395135cc5da724403ece4340f7e04c1ce752da15d0ad337591ec9c9/sqlalchemy-2.1.6-cp313-cp313-win_arm64.whl", hash = "sha256:bc495d00f235c3df6f376187a4ef48a56e162fdcc0a113ee34518332aa1aa2df", size = 2393224, upload-time = "2026-10-21T18:54:18.405Z" }, + { url = "https://files.pythonhosted.org/packages/d3/23/1d808dee65ee9a7fc0013d3f42af0655bc1e740c55b2ee8a2dfedc09be26/sqlalchemy-2.1.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5f9400b1234555b9ae78450123dec7aaf045d8d978a1e94967437ebe561ae120", size = 2471756, upload-time = "2026-10-21T18:49:35.133Z" }, + { url = "https://files.pythonhosted.org/packages/c3/33/f3feba2de61a20df6293d7da26eedabdd5fd01bb44f6b6a6a192a5a902de/sqlalchemy-2.1.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d106c2191a3a0dea6873c96ab909f31c13978913714a25023f492e9ccc8ffcc7", size = 4596061, upload-time = "2026-10-21T18:54:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/2c/40/42ccf6f7f6adea241f51f236868b8f27076f072b22e194b197f1bafedd75/sqlalchemy-2.1.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39ca3a128260fb3aa1c33554f1993bc5ddf3f82375697028c49e7b411ab80b84", size = 4625648, upload-time = "2026-10-21T19:05:24.248Z" }, + { url = "https://files.pythonhosted.org/packages/43/52/2007c76766751730e4ac51d5df9aa22be8c82a242413085f3a424d7efd7b/sqlalchemy-2.1.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8c34c200a124a1a5769671613b3fc05a21d0a50f7ad942cf0ee12d4390a112c", size = 4319086, upload-time = "2026-10-21T19:23:20.337Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/dd2b47ab0cb726abe54cc365a1d0bdf1a0424d936c740512173eaec7ab89/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:17bc5f508f5e5cee2722a5a14af0a48768b40e1fc5d1bfc92abf6ac587636bd7", size = 4525360, upload-time = "2026-10-21T18:54:25.782Z" }, + { url = "https://files.pythonhosted.org/packages/07/fd/6499902fd0fcf1dc078efc3d33c60434c55901c14dcf20bad98fca1ed556/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c757e13b63a25d59a2566c9b2e77adf14d969207a375ca82a9b4fa1a04b37845", size = 4320430, upload-time = "2026-10-21T19:23:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/5a/22/d805c0b3ff191af4e3cf8b2303c0fca44f5845d019f694c22b2d05e4a3b0/sqlalchemy-2.1.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:73aa8d2f911a54d1310e8258a752207e2d7624f4db46e2320d734fe7218320a1", size = 4586042, upload-time = "2026-10-21T19:05:26.135Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/015929abb68561990262348bd18aec9768ad743c293e77ad659947c20fa4/sqlalchemy-2.1.6-cp314-cp314-win32.whl", hash = "sha256:1927eabf26265ed962627e7a64626d9058da29cc0faea341091302ec108ce305", size = 2388927, upload-time = "2026-10-21T17:11:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1b/19bc3e0b590228bd122237bf38092b292c04e6501f9de7024a5b6d68cf10/sqlalchemy-2.1.6-cp314-cp314-win_amd64.whl", hash = "sha256:fd1154c6394892e727ff234273f5eb2b4319e5714917b016602435d77add8dcb", size = 2439897, upload-time = "2026-10-21T17:11:39.092Z" }, + { url = "https://files.pythonhosted.org/packages/6f/27/e01e46d55b60be7df1420604ad7d81ede3fbf5d41d9cdd7d2d25d5d98b6a/sqlalchemy-2.1.6-cp314-cp314-win_arm64.whl", hash = "sha256:10a4fe106adbb46c9452f750d8a5d4013d9a7e9752f63631c3b255c062b748d0", size = 2404309, upload-time = "2026-10-21T18:54:19.619Z" }, + { url = "https://files.pythonhosted.org/packages/bd/71/62984152c616b26883a686cf05d650f34782108c26c17d0946683b2f13c1/sqlalchemy-2.1.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea5f3dd7450669acbfee7813d3a2a13876806b1d9e4c1626a7bf587c33c68472", size = 2511148, upload-time = "2026-10-21T16:53:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/19/cb/fc1fe57bd16a0b3d4ccf8bffb2b664b3b9fb53ca87e38873413bcec3ee74/sqlalchemy-2.1.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4e4782514c16753f8bce3dd925cd37d26c847bead06e179af6fa4993ce83e88", size = 4927691, upload-time = "2026-10-21T18:59:13.924Z" }, + { url = "https://files.pythonhosted.org/packages/63/79/f155003077cee0893aca3f573e20d69ee28a6219966265b1be55c36a6f26/sqlalchemy-2.1.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da21b36d60f47dd04042e9372ffb3d1ededf5c60a76d43f8c3592221fb2d41cd", size = 4838397, upload-time = "2026-10-21T19:17:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/90/e2/f2787823ad943dbbdf6ceacd3a418bc5b633229e5f2d1d438eedf79a722b/sqlalchemy-2.1.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:048d1beb9bfd7ca1afbcfdb8da9519712827df6cccc80a225715ce387d37bb3b", size = 4516544, upload-time = "2026-10-21T19:44:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/9f/8c9eb3efec0c5909d7161c9c4039f9e0d108cb10b2e500f9191f3226a5b5/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8de444335e3723959913f6635d579276a3517b8c19cbdfaefb04e73b5480e88b", size = 4783970, upload-time = "2026-10-21T18:59:15.941Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/c80016c99d2ede933088a68dc2b198e1ba09c574d82213703ddd91962349/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:231d0fd85866f847c34c915116c386c83e677ab6f79a94c835b7ae7313b86095", size = 4516656, upload-time = "2026-10-21T19:44:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/fc/72/385e0312720af964a4d72f4a37f625fc966aed4115ea249db202521adb1c/sqlalchemy-2.1.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf937348d4052eac6cfa93c809ad214be75b1f057e3fc0dd2e374a814a23a16b", size = 4775302, upload-time = "2026-10-21T19:17:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/2e2eb1f296e4617fb11fbf12acc4aaf1be4e75dd3eed8693bc9967c596ed/sqlalchemy-2.1.6-cp314-cp314t-win32.whl", hash = "sha256:bc8b805fb04472e0f9cb3bc6aad90f24b202afc274b7b8bbd48042130e82997a", size = 2447775, upload-time = "2026-10-21T18:11:52.675Z" }, + { url = "https://files.pythonhosted.org/packages/5e/80/9bdce178d771f569cf3c88516cb7a71eaf6d701086fa47fe5bfb2b3022b3/sqlalchemy-2.1.6-cp314-cp314t-win_amd64.whl", hash = "sha256:84033cab9fec2a515a62d76a7b4acafa83339917db9ea911b65b017928ae7dd8", size = 2515411, upload-time = "2026-10-21T18:11:54.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/00/587dcab13fdb2ecc5854171a31eaa9ef5fc885bd1b034aa8d7e6ceba19cd/sqlalchemy-2.1.6-cp314-cp314t-win_arm64.whl", hash = "sha256:96ace18758136905f8d5588ec35d6ae4fdc29ce4e4902518263ca33abf10de04", size = 2433508, upload-time = "2026-10-21T18:48:27.552Z" }, + { url = "https://files.pythonhosted.org/packages/34/da/b04b7e7da0374d8e40a6d894fe9969a4d7477d4b5963c94736e85599d27d/sqlalchemy-2.1.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:89d05a1ef531a1fee86d68e731c428b773bd37aabf743bf63dc3709c86f7254d", size = 2471694, upload-time = "2026-10-21T18:46:27.122Z" }, + { url = "https://files.pythonhosted.org/packages/67/17/cc5203117626f71ab3a9ef2fee4cc5ec5c53a80d72b1dd791bc3d497afd2/sqlalchemy-2.1.6-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e12763cb2a400879c6dfe34995a032f707de635c8cad14d6ba777a1250be90b", size = 4599858, upload-time = "2026-10-21T18:55:38.095Z" }, + { url = "https://files.pythonhosted.org/packages/be/bd/a4d24b125b92bb156fe2aa3ce8894d19d7d015e246ce88baceb724776a26/sqlalchemy-2.1.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e1a9ae71366164800942c477e663b31f3accef55d2e039cb7919fdb54b0086", size = 4635165, upload-time = "2026-10-21T18:57:40.67Z" }, + { url = "https://files.pythonhosted.org/packages/21/12/34545d002189c49f05121ecc2b393bd2a5d2dbd9dc0c6e4510c95e3284e1/sqlalchemy-2.1.6-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9bd573f8da863781ace89950d57a959397d5048b1cf69931c29b2b4ce1405bd7", size = 4341217, upload-time = "2026-10-21T19:17:05.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/bf/491ed3e97d1625245c153c083064f36b1c3c1ecc1b1258a84a1f67f80f98/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0d0424a4189f95886c78b48bb500a1dd7ae6dbae76deb552b15813dc298d9c8f", size = 4528088, upload-time = "2026-10-21T18:55:40.234Z" }, + { url = "https://files.pythonhosted.org/packages/10/7f/8750ec25961075140def7fa15f3d07d800a68bacfdbc90112387345b78ca/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:cc60fffea328f38187a4fce43f99c6bbf930de2b87b78251f43f2ba64a1af622", size = 4340063, upload-time = "2026-10-21T19:17:07.687Z" }, + { url = "https://files.pythonhosted.org/packages/62/5a/a9c53bb9dad2732de73d835f691784a3153b4ed693fa385534d2bba39c25/sqlalchemy-2.1.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c390f76f2fe408aa1a3121f3ca3d5c15698e14d913197417355fe3dc016f787d", size = 4597401, upload-time = "2026-10-21T18:57:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/18/02/bc6b004cd8e5906efca1dbbb4e158aa2efe31a5623c32946ec9da73126ae/sqlalchemy-2.1.6-cp315-cp315-win32.whl", hash = "sha256:79c1c20dffd4d8d7e669f096c88f261da8ba3646939cd3eb6faeccb92bae99c3", size = 2388678, upload-time = "2026-10-21T17:51:54.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d6/13087d9a1ded5bf8ccadc2f315c157d4796828829b2c2d6cf1031f1c00ba/sqlalchemy-2.1.6-cp315-cp315-win_amd64.whl", hash = "sha256:f1e78019147ff7e08afaffb88fc3a0aa795171b147eacfc7ff873ce7a0d37dff", size = 2439956, upload-time = "2026-10-21T17:51:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/6d/90/263b259d872cf4e11c7bcd1b278256096482db06fa23ddbc8c00c9918f3d/sqlalchemy-2.1.6-cp315-cp315-win_arm64.whl", hash = "sha256:3974e33fc322b645eedeac8a3dea93022466fee41a6385c744a6656aba293cd4", size = 2403996, upload-time = "2026-10-21T18:50:02.367Z" }, + { url = "https://files.pythonhosted.org/packages/04/60/b4821fe7a42405d12abf9ba48e13b5e056e428d6f0e48c8166f0c7df61fa/sqlalchemy-2.1.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e269c68ec200538ae3fa7f9e8d8ed2b4b34d974c25945286ccc2a488438acf32", size = 2507298, upload-time = "2026-10-21T16:53:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/4feccf4511123d55e26269aa22c426ca2cdba88298bd65781bd3609e0e1b/sqlalchemy-2.1.6-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25199a1b499da643de52ab73e972eb07e55e2a95bb99eaa491846098fa2214c", size = 4905036, upload-time = "2026-10-21T18:59:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fa/7329c40698034056c924210eaba019f46f2914783c3d52b29c47806399c9/sqlalchemy-2.1.6-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:641a3742dac4b8f325667b585097de0f1d20eb3498506c92d5f04c20b3a6c823", size = 4854715, upload-time = "2026-10-21T19:17:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/79/66/098e0981ba55bf61a4b85ae904714987640a7f489da57641c8b6f7d1cfc6/sqlalchemy-2.1.6-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d96186af967d7ae3f76d1e9a8fb70b5beb0f0dbdacfd3b418a946fffbb8781ff", size = 4502934, upload-time = "2026-10-21T19:44:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/20/69/f7ef99af2f3b17e45c1189176decaa2be595848f67827dd7c665ec8e2166/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:5a9b4cbc5f5cda3c9429e1ac503a57f3b95c59886f26af6ffb44e2e7628b7df8", size = 4760666, upload-time = "2026-10-21T18:59:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/72/d7/3dbbefd2342237fbce9b9169ac4f425d039e97b8a9f765467e4a0319c9aa/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b7ddb305126d34fda64c3fe4632a43fb9b724da6cc7d1dedd8434e6c783b285b", size = 4502218, upload-time = "2026-10-21T19:44:47.119Z" }, + { url = "https://files.pythonhosted.org/packages/a4/22/07acba0614b788f94c655a25d76188591921a459fe4f4925a3176b576328/sqlalchemy-2.1.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70b435f719d7156623f49ac6539f30dd8471500b5acf399f3b318c1c17f6fa7f", size = 4780103, upload-time = "2026-10-21T19:17:39.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/0d/ea88e6e0e069b6c1c65c7a32043ab2203386c80d0bd1a25cf1ba39007741/sqlalchemy-2.1.6-cp315-cp315t-win32.whl", hash = "sha256:654192c9f192459ee5f63e6606cc6dd8c50681c28e0689d2a494fc9522b357c1", size = 2444581, upload-time = "2026-10-21T18:11:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/2182431a4955adfeab69a89e7e4e81eaabbfbf1846ff8634b0f9448fd6cd/sqlalchemy-2.1.6-cp315-cp315t-win_amd64.whl", hash = "sha256:a053a9d9c9ccc9b72d90ecdcbabcbe57446defc7e8c52fc9978188f78742ac19", size = 2510428, upload-time = "2026-10-21T18:11:59.23Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/e290384d311e6e2c495734673bdbd2e358dc6725e7c1525326eb8e34af8d/sqlalchemy-2.1.6-cp315-cp315t-win_arm64.whl", hash = "sha256:61dc3fa488208a2f03e0e25ad32950dc024545291acc7348f2e550ea1bc5abb4", size = 2429115, upload-time = "2026-10-21T18:48:28.987Z" }, + { url = "https://files.pythonhosted.org/packages/c1/69/c75534e336b06b3fd3f76de73d89736510800e60c77c58d4e4ce18389018/sqlalchemy-2.1.6-py3-none-any.whl", hash = "sha256:106878cf3795cd3d357e2e858609f9e44f20f19b2d8e48a3532bef3f90b03fbe", size = 2063055, upload-time = "2026-10-21T18:42:10.234Z" }, ] [package.optional-dependencies] @@ -5898,11 +5670,11 @@ asyncio = [ [[package]] name = "sqlglot" -version = "30.24.0" +version = "30.27.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/7c/9fd907983ff041692399bf855d344e1fcc9a02c8d92a5fee6dbdf139d19e/sqlglot-30.24.0.tar.gz", hash = "sha256:6451aabd1c9994d0f73355c962a5eaf9258381b3f2554e53db916cd9a605552a", size = 6131857, upload-time = "2026-10-20T17:14:03.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/322dee46e3fd283a3b249a22e4f369693f9aad9fdc714df66b815882e99e/sqlglot-30.27.0.tar.gz", hash = "sha256:10617b2beee196e88a310c6a5348482248bf10ac01c6ade22128bf4a690ec0ee", size = 6172431, upload-time = "2026-10-30T18:18:59.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5f/d78f74306cc026c5f40f8808a47356dfd37461eb15eb759a20e5e9efe7d7/sqlglot-30.24.0-py3-none-any.whl", hash = "sha256:9488828cc018453b7a3ba01e65d2749c9a731314c14583695163ca1f358203fe", size = 792162, upload-time = "2026-10-20T17:14:02.053Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/fb7ce53c23e171ad059fd3a5ff6889399da2804cf0cfda2a10c64f32fdf7/sqlglot-30.27.0-py3-none-any.whl", hash = "sha256:b630686fc167762e20960ad3d007ad03d81af0a804c9f7d117f1fe06433c839e", size = 801972, upload-time = "2026-10-30T18:18:57.65Z" }, ] [package.optional-dependencies] @@ -5912,33 +5684,33 @@ c = [ [[package]] name = "sqlglotc" -version = "30.24.0" +version = "30.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sqlglot" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/cf/5716822a3f35af1ab7b86edc86e82c90e4a6eba2088edf2370491e378b94/sqlglotc-30.24.0.tar.gz", hash = "sha256:e708b7ada13ad1509dfbefc45a35368726e4dfec8323036fda6647dafc9a6937", size = 544200, upload-time = "2026-10-20T17:12:47.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/dc/a6acb4ad3571486c7cf4545673ff2615c095b29fee04c93fd4eb5201bdd3/sqlglotc-30.24.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:44ef606f6daa60d8d22358c28ab3664e5957c71062d811a7fc59043fd2d04af6", size = 33962047, upload-time = "2026-10-20T17:11:28.472Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/6ab3ae7a6ea4e407fc70f8bee9b504f8e2287c6023b3377b8dd89f548fa1/sqlglotc-30.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:932750245cae2b02a2c035695fd87fefdf5bc8ad08f7e329722e2fa71ec5bf92", size = 26424854, upload-time = "2026-10-20T17:11:32.179Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e1/372ebe5b13a445fd99cf26f08114a24ec39f181928d0f42c2dd17056a567/sqlglotc-30.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc54fe660ab21be7b002cf5299c61d1a1586f4936647c84c7dece07b30b8b700", size = 27669243, upload-time = "2026-10-20T17:11:35.916Z" }, - { url = "https://files.pythonhosted.org/packages/e3/0d/beb6a6683b499bbc5feb97f897542848e4f36a89c8204a1351f161d65b94/sqlglotc-30.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:21e73f216dce7356a9b7628713306736306be002dea30515f5f77d6c7665f82e", size = 11412569, upload-time = "2026-10-20T17:11:38.842Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a8/e2c469c1c5f86d7c5d3bfb58560b27faeac3246c4d2f44bcb56cecde5de3/sqlglotc-30.24.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b648130d2fe20262679301cc01dadbcca3d9dfe9e64a59bb693cedcb4da86a5", size = 34248101, upload-time = "2026-10-20T17:11:42.873Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/3fb061495cdf83cf089fe9466c33ab22ae2fa6fc0baaa7c161e956b717db/sqlglotc-30.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6200f995971741b1999b7069f43d70acc993398f242b2e544ae2b1097144bb7e", size = 27388073, upload-time = "2026-10-20T17:11:46.65Z" }, - { url = "https://files.pythonhosted.org/packages/01/31/e695a9e0d524a995ed0dffb787a6e9f4a9c9fd1896789bbcd3f9f8b47325/sqlglotc-30.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16c705dfb8d158c0be093f421de97337a4415b68752eee438eec1da699f9ea3e", size = 28780895, upload-time = "2026-10-20T17:11:50.548Z" }, - { url = "https://files.pythonhosted.org/packages/35/5f/352be799728e1677ccd6d006e29ee45c3c4c3b8ca8bf7b021cc79ef99d98/sqlglotc-30.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:e3bb24b3542a8fef9b5b97525fdecc4a384665e7f487ae48bd8c140e396820c7", size = 11635988, upload-time = "2026-10-20T17:11:53.294Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/63e8ebe925004b7acee1737ad7c01c6bb9bf67cc63b681a3071caeebae44/sqlglotc-30.24.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:896df3e3a34c909b272b096129a14c5d44ab3ab0a26098a2e9ede1180f78f9f6", size = 33683535, upload-time = "2026-10-20T17:11:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/68/c4/bce59317429997961658a21d5ae7a87a1758e86b1938d14fa2043ac27e73/sqlglotc-30.24.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caa4467247fffa2643f6e3ff6859ccbde4ca26c0b582f55eb2c9028b891d4e7f", size = 27014217, upload-time = "2026-10-20T17:12:01.045Z" }, - { url = "https://files.pythonhosted.org/packages/32/84/d7d20cc1f91c187837104d475703ddf6c4d29e3f37415bba1054a5deea21/sqlglotc-30.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2902a1022baba90b780de3fcd502b0baa4fb5291ed5667782131da6146a38889", size = 28410483, upload-time = "2026-10-20T17:12:04.959Z" }, - { url = "https://files.pythonhosted.org/packages/11/31/7a8ffbbd72740ef948c63cde30e737534ef4215861efe1a11271708d373d/sqlglotc-30.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:91b528983e031afcf7558d9916c8858cd182d9c6f7a763d39fe007e9ddcea7de", size = 11647757, upload-time = "2026-10-20T17:12:07.925Z" }, - { url = "https://files.pythonhosted.org/packages/fb/22/2411ee5450ef8bb40cf9dabcaaab8533a3d4e667472d8fda56e6afb1cb61/sqlglotc-30.24.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:205b7099fa264727681aa796ccff12bad047d222113c3381851ac4c10d646bd9", size = 33609831, upload-time = "2026-10-20T17:12:13.892Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7c/47e97998ea74600afc8834052a93d523a5ec963de5fb2f528cfc6c0adace/sqlglotc-30.24.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9e11c30adb6db4aaa267a29f953fa3e40802d7f973c0897c63f774af11ea27", size = 27002208, upload-time = "2026-10-20T17:12:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/19/df/b08d263f3b03409487c3b144bdbbce7dfce921579539b9f927c6fdbee2c9/sqlglotc-30.24.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e01e93ad36252c21f577de131d0343087946f110fd627f501576cebeae6e2258", size = 28297247, upload-time = "2026-10-20T17:12:24.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/ba/7d4c520d5f6d516af818561310a45255e2b1a8cc5098628c360b6e9a29e2/sqlglotc-30.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:af9f153acb00075c90f4ca3791b4f87c4f87834f5424ef80531346819e61135e", size = 11801223, upload-time = "2026-10-20T17:12:27.598Z" }, - { url = "https://files.pythonhosted.org/packages/a4/10/1b55409b7e9a9d7fbd94e32373603a508579a27b438a573e07000585dabd/sqlglotc-30.24.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:192ba229a172be968b9764e6a3ea5501b5ffd9649ce6083bde40d1f87881d152", size = 33573842, upload-time = "2026-10-20T17:12:32.671Z" }, - { url = "https://files.pythonhosted.org/packages/76/da/2662df113f44dfbfaf33990746320d87fce865bcbe6fc0ab7623a3aeada1/sqlglotc-30.24.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ed253e48630c02ce4e631bb5539c658a9757c92b84aa31c6f1aa39d99e2ccb6", size = 27080224, upload-time = "2026-10-20T17:12:37.67Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f9/6a9342fc0c09e8bb8c60c6430dc42c9b89f9db9957929b02021cc93c087d/sqlglotc-30.24.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b059802c5a72ac4d4c9e0bcddd88b0e25791d00cafa1e8ac635a364f87062bc6", size = 28267474, upload-time = "2026-10-20T17:12:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/35/79/ac341ef1843f9c450525172d8960c54d5fc90a7ff094cb138e5616e0a473/sqlglotc-30.24.0-cp315-cp315-win_amd64.whl", hash = "sha256:9cf0ec6f354ee33fc613d0d7b4a2631259a122e1eb2b8d9a25c35cd1d8b6e6f5", size = 11801325, upload-time = "2026-10-20T17:12:45.413Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d9/b0/1f1d89edf9336132b0a10b93a8f714aa7367a34ac6fbf8b592930ff0e3bb/sqlglotc-30.27.0.tar.gz", hash = "sha256:fc7d094eafa7b4734035e6c284f3e6c4094e15a9fc2c77e1031bfd70c712b46d", size = 559487, upload-time = "2026-10-30T18:17:57.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/0c/b397fe02d5a41979288d725409a521b869a8d8d8a2264b8a93e9ef8a0fde/sqlglotc-30.27.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e11593cef551abae0814b1aee136791a57042e43cc8eb6be9d22cd84f34475e3", size = 34212747, upload-time = "2026-10-30T18:17:04.329Z" }, + { url = "https://files.pythonhosted.org/packages/a4/81/2c28a95a470e3ee7d844ece053da340c0265b20460f4eb47d8fd285d5f65/sqlglotc-30.27.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58d4c2f82fde17b47e77cd883d001bdaff4eaa081fe8436aec12f3d700d2c79a", size = 26585089, upload-time = "2026-10-30T18:17:07.682Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3c/42b97fde129bfc4151d3327ec105433b2eb1ed34b7fc6dba05237e267230/sqlglotc-30.27.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976c123637cf9ab0a255f4c94fc51c8c6fdacc8980d0d5d83c3a5800a51c590e", size = 27843396, upload-time = "2026-10-30T18:17:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/91/7d9d47754d7a3c55aca3ccc72bed8125256bccd0cef473179ffd75e582ba/sqlglotc-30.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:19ecc02896025a3e5e7b8abcc3b19649c71e92d4b628b510cd24ff33b021594c", size = 11514157, upload-time = "2026-10-30T18:17:12.461Z" }, + { url = "https://files.pythonhosted.org/packages/d0/74/8d03cfdac89db8217cde36788e4f090702f0564462dbc8c4ac1e964ceb2b/sqlglotc-30.27.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b9940c94761840e73982f6dbfccd4887aa270b568ff7f31aea6c0e51eed7e65", size = 34496926, upload-time = "2026-10-30T18:17:14.819Z" }, + { url = "https://files.pythonhosted.org/packages/60/e4/14c4f28a40db4804f65fd12db64a026b723257a88029c0992e3ba021b073/sqlglotc-30.27.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d61152bbf79df38bb4d456d475757149138c471cdb6cbfaf57cd63f39f90199", size = 27570066, upload-time = "2026-10-30T18:17:17.661Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a1/cf50d5c50971296243202960979a6b3df6ea1a85fcaf9019e5b6df10cc55/sqlglotc-30.27.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bd9e4bc2904ad3e73143a271623485a2b71b8840da50d4ecdeb9d95f98d6bb8", size = 28960561, upload-time = "2026-10-30T18:17:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/3e0846067c52b2198e6348cd27d9dc76a4f0ea6ef7aea51b9a779321463d/sqlglotc-30.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9b6d12c8b58efaf1d955cf44a3bf6411af59113662fd57af80340ea029ec767", size = 11740341, upload-time = "2026-10-30T18:17:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6d/9b67846fd614946befaf00ef8da1eeddbfdeec367bbbc4ba38ddf42f392f/sqlglotc-30.27.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:31913a3aca30add130adee189471c0409dfbfe091c0824e59d7a8d0d3b72d98e", size = 33936648, upload-time = "2026-10-30T18:17:25.255Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/01a6da35d6175f031ffa352b0f55f4e2d23c259506ea2b2202bc038fd72f/sqlglotc-30.27.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:403143cf7844527d20a291f4f99f22211cae849d411e921b003116d90280514f", size = 27197058, upload-time = "2026-10-30T18:17:28.308Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/daff3fef419807d2f68cfd16c07eee1e2c354c2903dde8ce54c98d7ff1f9/sqlglotc-30.27.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94dfb27809552a792ec4df9ab951c6fe68ccf147f1ae1bc19c0067ab04033f1b", size = 28588139, upload-time = "2026-10-30T18:17:31.06Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/53d698c9ee841f17e59d93ea90df8f763ff4ba5712c12de1ab599ffa078d/sqlglotc-30.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:d38adfac0ab96e75b74e597d274f7f658baf4f35ebab99739c150e13d2d09073", size = 11751835, upload-time = "2026-10-30T18:17:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/12/ec/63add017aa59b71ab222910d71ed238f21a28e699c96fca4b012f63f2320/sqlglotc-30.27.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c73be857191c991f62081ca4443e5c5e65863ebc0a20023a66915e041c5a0848", size = 33849986, upload-time = "2026-10-30T18:17:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ef/7d70da298a94e6c63dce27aee4a1f5b76d88f6c769d7f6ce083479342a27/sqlglotc-30.27.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27c8dc6d61ce2ca81e32bae3bfff9e544a39377978ba3cf85f113a3c263d743a", size = 27177684, upload-time = "2026-10-30T18:17:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/29783a6d0fe80303b28d8a55cc24c1dc49b9eb181096af08f66d38dedd22/sqlglotc-30.27.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:919ba8ca46fddd8c574cc137f06d652a9123a7f8e8ed816edcf69a85abe2838a", size = 28473877, upload-time = "2026-10-30T18:17:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c2/e21112da8f1aabff5563dd0e55528db0767c36e67d597b982f210bc342ea/sqlglotc-30.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:c147f8c294869030a4186d1a9c5f7c0cd8b884a198eb5dbd73ab72278831f943", size = 11908763, upload-time = "2026-10-30T18:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/88/15/5a4d52cb0019b478f6db66f478e3ac2812b83e30bd1f7731cfa248c949f8/sqlglotc-30.27.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c89f19332378f4bdfbfdb0b393ec91bfb5e6af091698aed4db4c984efd40a736", size = 33819082, upload-time = "2026-10-30T18:17:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/94/03/327720ff687dbc588499a043c1b7ed4cab4d3b64257b0642efe58952680c/sqlglotc-30.27.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11747c395fc48b5fec9a50f35680865297fd1dd31f86997048676e8f83193d0f", size = 27260537, upload-time = "2026-10-30T18:17:50.389Z" }, + { url = "https://files.pythonhosted.org/packages/23/4c/4dc659a5d96fcf6da33463757ca4cf7615e110eb5cbd96d25715a56f33b8/sqlglotc-30.27.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:265b261cc8d55450b73b46c34737b543be1dac6e1c7aa547cad896dbdb619fd7", size = 28454140, upload-time = "2026-10-30T18:17:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/56/ea/af129cf3371a4e606daea192cad2d2424de040e90923c9e3aedf4005f16d/sqlglotc-30.27.0-cp315-cp315-win_amd64.whl", hash = "sha256:564cfc4ea7e838959b2ceb69b24e11f4290c311b2a12ba7c4d54184cda3e0086", size = 11908824, upload-time = "2026-10-30T18:17:55.819Z" }, ] [[package]] @@ -6122,9 +5894,7 @@ dev = [ { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, - { name = "auto-pytabs", extra = ["sphinx"] }, { name = "bump-my-version" }, - { name = "click-extra", extra = ["sphinx"] }, { name = "covdefaults" }, { name = "coverage" }, { name = "dishka" }, @@ -6132,9 +5902,9 @@ dev = [ { name = "fsspec", extra = ["s3"] }, { name = "gcsfs" }, { name = "hatch-mypyc" }, + { name = "mkdocstrings-python" }, + { name = "msgspec" }, { name = "mypy" }, - { name = "myst-parser" }, - { name = "numpydoc" }, { name = "pandas-stubs" }, { name = "pgvector" }, { name = "polars" }, @@ -6143,6 +5913,7 @@ dev = [ { name = "pyarrow" }, { name = "pyarrow-stubs" }, { name = "pydantic-settings" }, + { name = "pymdown-extensions" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -6154,19 +5925,8 @@ dev = [ { name = "requests" }, { name = "ruff" }, { name = "sanic-testing" }, - { name = "shibuya" }, { name = "slotscheck" }, { name = "sniffio" }, - { name = "sphinx" }, - { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-click" }, - { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, - { name = "sphinx-iconify" }, - { name = "sphinx-paramlinks" }, - { name = "sphinxcontrib-jquery" }, - { name = "sphinxcontrib-mermaid" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "types-cffi" }, { name = "types-colorama" }, @@ -6174,24 +5934,15 @@ dev = [ { name = "types-psutil" }, { name = "types-pygments" }, { name = "types-pymysql" }, + { name = "zensical" }, { name = "zizmor" }, ] doc = [ - { name = "auto-pytabs", extra = ["sphinx"] }, - { name = "click-extra", extra = ["sphinx"] }, - { name = "myst-parser" }, - { name = "numpydoc" }, - { name = "shibuya" }, - { name = "sphinx" }, - { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-click" }, - { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, - { name = "sphinx-iconify" }, - { name = "sphinx-paramlinks" }, - { name = "sphinxcontrib-jquery" }, - { name = "sphinxcontrib-mermaid" }, + { name = "mkdocstrings-python" }, + { name = "msgspec" }, + { name = "pymdown-extensions" }, + { name = "ruff" }, + { name = "zensical" }, ] extras = [ { name = "adbc-driver-bigquery" }, @@ -6333,9 +6084,7 @@ dev = [ { name = "aiohttp" }, { name = "anyio" }, { name = "asyncpg-stubs" }, - { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, { name = "bump-my-version" }, - { name = "click-extra", extras = ["sphinx"] }, { name = "covdefaults" }, { name = "coverage", specifier = ">=7.6.1" }, { name = "dishka" }, @@ -6343,9 +6092,9 @@ dev = [ { name = "fsspec", extras = ["s3"] }, { name = "gcsfs" }, { name = "hatch-mypyc" }, + { name = "mkdocstrings-python", specifier = ">=2.0.1" }, + { name = "msgspec" }, { name = "mypy", specifier = ">=2.0.0" }, - { name = "myst-parser" }, - { name = "numpydoc" }, { name = "pandas-stubs", specifier = "<3" }, { name = "pgvector" }, { name = "polars" }, @@ -6354,6 +6103,7 @@ dev = [ { name = "pyarrow" }, { name = "pyarrow-stubs" }, { name = "pydantic-settings" }, + { name = "pymdown-extensions", specifier = ">=10.16" }, { name = "pyright", specifier = ">=1.1.386" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, @@ -6365,19 +6115,8 @@ dev = [ { name = "requests" }, { name = "ruff", specifier = ">=0.16.0" }, { name = "sanic-testing" }, - { name = "shibuya" }, { name = "slotscheck", specifier = ">=0.16.5" }, { name = "sniffio" }, - { name = "sphinx" }, - { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-click", specifier = ">=6.0.0" }, - { name = "sphinx-copybutton", specifier = ">=0.5.2" }, - { name = "sphinx-design", specifier = ">=0.5.0" }, - { name = "sphinx-iconify" }, - { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, - { name = "sphinxcontrib-jquery" }, - { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, { name = "sqlalchemy", extras = ["asyncio"] }, { name = "types-cffi" }, { name = "types-colorama" }, @@ -6385,24 +6124,15 @@ dev = [ { name = "types-psutil" }, { name = "types-pygments" }, { name = "types-pymysql" }, + { name = "zensical", specifier = ">=0.0.71" }, { name = "zizmor", specifier = ">=1.28.0" }, ] doc = [ - { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, - { name = "click-extra", extras = ["sphinx"] }, - { name = "myst-parser" }, - { name = "numpydoc" }, - { name = "shibuya" }, - { name = "sphinx" }, - { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-click", specifier = ">=6.0.0" }, - { name = "sphinx-copybutton", specifier = ">=0.5.2" }, - { name = "sphinx-design", specifier = ">=0.5.0" }, - { name = "sphinx-iconify" }, - { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, - { name = "sphinxcontrib-jquery" }, - { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, + { name = "mkdocstrings-python", specifier = ">=2.0.1" }, + { name = "msgspec" }, + { name = "pymdown-extensions", specifier = ">=10.16" }, + { name = "ruff", specifier = ">=0.16.0" }, + { name = "zensical", specifier = ">=0.0.71" }, ] extras = [ { name = "adbc-driver-bigquery" }, @@ -6457,29 +6187,15 @@ test = [ [[package]] name = "starlette" -version = "1.8.0" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/bb/3d8757f21e9a53d9035c9eb039a777047139a16bfdbedfc7a58241c3d914/starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b", size = 2734356, upload-time = "2026-10-24T15:04:12.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" }, -] - -[[package]] -name = "tabulate" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, -] - -[package.optional-dependencies] -widechars = [ - { name = "wcwidth" }, + { url = "https://files.pythonhosted.org/packages/88/2e/947cfe02def9371c07cd5bf5b6b61c829e9aecb9904db02a66cfafbc1eec/starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4", size = 80571, upload-time = "2026-10-24T15:04:10.371Z" }, ] [[package]] @@ -6709,11 +6425,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.5" +version = "2026.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce71b6e50a9d09445aa2ecbd08359a337976627b8409e7/tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", size = 200404, upload-time = "2026-10-03T09:23:14.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/29/6a1391d31cd00ff9e927215a224a880e1e84e826b64ba80a499e51c074d8/tzdata-2026.6.tar.gz", hash = "sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d", size = 201017, upload-time = "2026-10-28T09:57:09.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/9a06c2227fedfd4919f0eaaa8b94377d9730882a7e4f95823c9a331ab5e1/tzdata-2026.6-py2.py3-none-any.whl", hash = "sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec", size = 348210, upload-time = "2026-10-28T09:57:07.276Z" }, ] [[package]] @@ -6817,90 +6533,62 @@ wheels = [ [[package]] name = "uuid-utils" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/35/2e9666504bcb3ab50656b86cd468880cedb86d3c97b88d3805dcc124b95d/uuid_utils-1.0.0.tar.gz", hash = "sha256:8ed2e0156d29c4cfa0f931b4b71b35d2705d84054f63ba07a78f7acc2eb09a5c", size = 43759, upload-time = "2026-09-08T13:27:28.344Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0b/9dd7618399c34481b4bad422d8c15c69a659bba8b1f1c85aa3198cb0cfbc/uuid_utils-1.0.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:55b8912f743b0026aa72a5ff392831610a240fb48eeeccf945db60f74645cd2f", size = 564415, upload-time = "2026-09-08T13:25:24.193Z" }, - { url = "https://files.pythonhosted.org/packages/03/00/6cb04489068dac102ba11e089fdaba32caaae79edc94ba02c34b7b460278/uuid_utils-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2ca478b10c68b57e47f22c5baf4ffe7c4ac922db776573840efbd4b34dfe4cfe", size = 287836, upload-time = "2026-09-08T13:25:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/12/f1/01c95c433a52a0af7b03d1e54f3a5546f22c2e0bee769e3fda4aaa7c11e4/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66cb850461fd4786da6452a8ee1a7efe882de1ebe395ddfb18f27b25ad96646", size = 326987, upload-time = "2026-09-08T13:25:27.39Z" }, - { url = "https://files.pythonhosted.org/packages/b0/67/a1d23ee04a10631b2ffcd014aeb57d29bcac35347218f6e07f5f4f7890ea/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa81372e1e52bcfb6c68bd77f194a87fec3aa254a5c87034aac71dcd232dd5e4", size = 335823, upload-time = "2026-09-08T13:25:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/91/61/ea0d8b5d04fa5f9c8f54e637aa4576522f1fbda62e1020352c9906fb84f4/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bbc1ea51dfe0cd70c1d1aeb493c70f7f55d3d82eb04fa118c0245715f22a312", size = 451699, upload-time = "2026-09-08T13:25:30.476Z" }, - { url = "https://files.pythonhosted.org/packages/63/ea/e88474c99b4b3b23f8dbfdb9001e649fe7ca933453cc5771b52583e0705d/uuid_utils-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4857774b37bace3f02059e434642bd30915d833427bed44e9ee061e567e69ec", size = 328498, upload-time = "2026-09-08T13:25:32.14Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9d/63469d3e5c22ea141fdbab03893960c6c9efa35191198729c06047bdaf73/uuid_utils-1.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d379ee954edc03653b89b4e3bfa0e1075a5d93220fc6f75e4a449c61467943eb", size = 353647, upload-time = "2026-09-08T13:25:33.513Z" }, - { url = "https://files.pythonhosted.org/packages/48/98/522a446c61887366da0602b17316598d85c58fb328ff9ea803f2cdff7ca8/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:85152c854e28f9ae66c204ccd003ebad64529c0e3d69f4abd4abb4da81fa6f4a", size = 504592, upload-time = "2026-09-08T13:25:34.966Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3c/f3163e1191596a79a575b102bf0a268a5a085f2478e06abdf7d1e89b4aa4/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fcbca750bc45a2447c077d19b77eaa2b9dc89f80c3552cdd0de8d0d0a6194e06", size = 610920, upload-time = "2026-09-08T13:25:36.719Z" }, - { url = "https://files.pythonhosted.org/packages/e2/96/2311bc6fc1f29cd2d25285584a3d10a90e2118b318d8a439f678c0a17da0/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bbe412cb4d2d52e0fa7d6bcaffe7985f06f1e97021e180b77a721dcbdb44ca00", size = 569925, upload-time = "2026-09-08T13:25:38.095Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c9/aa4836257ca94bb31e66fda030e645a35e64ad9737e43b6c30d6d0b2fef9/uuid_utils-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1aadb47e436f6624f3d5eccffdb0806dc820df724dbf4d810d0cb3fa9d3e888b", size = 533882, upload-time = "2026-09-08T13:25:39.523Z" }, - { url = "https://files.pythonhosted.org/packages/29/69/7e45f40c3441fac3fa97c592580db51ddeb40cd4e01772c8fb6f6f0da152/uuid_utils-1.0.0-cp311-cp311-win32.whl", hash = "sha256:20d82f23c2879140b5b6338c4e2f8f9e56c8af7dcf5aab24a90c8c3629bc3d67", size = 173355, upload-time = "2026-09-08T13:25:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/82/97/7b6038fae08834c66a083549e41905fc5376589c9d9bfeb28c3a6f678d5e/uuid_utils-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:91c2fec6dc8633a1e15f4774e6ba36b3584c15c9b66298dcc4628c2c1009fd94", size = 179819, upload-time = "2026-09-08T13:25:42.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/af/cc6fba9782410132d8352ef0da0f5d09572589d4a74b5a4c8108b3da5b88/uuid_utils-1.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:cf29f16b2675a11429576c72922bb9008a5f46b16ad5dc42415c91142fb4554a", size = 178297, upload-time = "2026-09-08T13:25:43.78Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b6/57ecfa3d19021dd361d54c3213fa504122e24fcf379a6804c9077b2ec8cd/uuid_utils-1.0.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:20d6b4ea345912ecf2ba0dfc0bee0714c40b092830f814f1b768c33a55a38da0", size = 557800, upload-time = "2026-09-08T13:25:45.277Z" }, - { url = "https://files.pythonhosted.org/packages/78/31/fc8cab83464720c384082398f96c25b2b77de327485d436cfc73aba21358/uuid_utils-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:75f132bd55a715d091e8b2e91a118862a203777d34c4b2794caa218de0cd947b", size = 287237, upload-time = "2026-09-08T13:25:46.672Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2f/496b126dd703e12b33637246793abd97e05fa163e764ada0be4abca37056/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ad43743b1b6dd791798c37a5163d236d57705fa32944cee35c5d4f06c23009", size = 322768, upload-time = "2026-09-08T13:25:48.196Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/627a187b22b97b29aa7f3af02edd898fcb33c472c8c4898c6f5103fb868e/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:129d2e1245c0f54282cdfcfb9498342808fdf9259782aa01e09d15e4c51b8a83", size = 330985, upload-time = "2026-09-08T13:25:49.778Z" }, - { url = "https://files.pythonhosted.org/packages/f8/20/5bf65a065f369ce0fd8a031688c2767265ae3ee0e6293002633e2fd1dbdf/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64afb1db6f732b9526cda719275922ab9e3a4ff7dd255b89f40709e65c70dbb2", size = 445612, upload-time = "2026-09-08T13:25:51.167Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d6/d3dc9b10ac5d6453d5225459b5bc2a2a7a9f53b7139d13727aa64661c2da/uuid_utils-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95b5ec6b070e5e3f3e02f7d195a22b90c1037afbc41006f122fb6d0499938276", size = 324362, upload-time = "2026-09-08T13:25:52.636Z" }, - { url = "https://files.pythonhosted.org/packages/11/0c/aba31a49583a59dd9022136a0032ffc6a8bda71d7f894da289aaa63f48d6/uuid_utils-1.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f401251b95ddb077daed0871d4f43b4a7af88c80da595329206e734b4629e7d", size = 347120, upload-time = "2026-09-08T13:25:54.449Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/ebefd7241f763ca0a37ec7caad67e2d84e311335f74c5f3cf6ec52ce2e2f/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d8b55e9759506f5b5849747f6fef927d3d81970d8c20285a99e797119ae3ca5", size = 501346, upload-time = "2026-09-08T13:25:56.135Z" }, - { url = "https://files.pythonhosted.org/packages/47/0e/7d155c4ea6af24eab30ff926d737d028f4cb356f6446f23efc9cbefeb44d/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:2f9b1f16576237171e2782390f95d888dd7adce851fc85016e4b7b25d1c89fc0", size = 607597, upload-time = "2026-09-08T13:25:57.785Z" }, - { url = "https://files.pythonhosted.org/packages/88/79/87708b9b618a29883d6b7be62569972aa5f7bc4af63338bc496532b5640b/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2ee3fbcc187d2b46e1bbac64203fca75b24451f1d39a436b8196d1445bf79014", size = 564136, upload-time = "2026-09-08T13:25:59.424Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/0f53c4954944311d51fd6ae9db25ad705e6c433d0db04c753b0743aa2c2b/uuid_utils-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:083b21bba1adef508f84f8ef8da3fe2f085bd95988b1c2333618f06a804efd16", size = 529072, upload-time = "2026-09-08T13:26:00.857Z" }, - { url = "https://files.pythonhosted.org/packages/be/5b/4124fc1794ce8ed5fe4ced71ececdc0cacfb1d08c6d192c735402ae49d58/uuid_utils-1.0.0-cp312-cp312-win32.whl", hash = "sha256:e8b27a32095b43eb9e4abcc297afc4d4f4b130e9fcf9c9d09f93eec1382d1f8c", size = 170398, upload-time = "2026-09-08T13:26:02.437Z" }, - { url = "https://files.pythonhosted.org/packages/83/23/f1eacc16c91cd78ff86e62990b650adebf65782c2b992564c41311201662/uuid_utils-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac9e2ed981262301c85e27520b2564d03102bb8be95130e3085f0060add619be", size = 175625, upload-time = "2026-09-08T13:26:03.771Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9a/729645992d308d2806043cf5e8add10413b072574e0a25b3fc014053cc5a/uuid_utils-1.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:6988755b05dd27af81da59ae1bf15b14226e824d87dd98b60559d116df6826d1", size = 174128, upload-time = "2026-09-08T13:26:05.089Z" }, - { url = "https://files.pythonhosted.org/packages/c6/59/950f27905400b098797d8996d914fbc7faf73e4eb7be2ed5b5bbc16005eb/uuid_utils-1.0.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f82137ecd4ffe69134ed632ca865587dd44ddc5caf512cfefe181c0f6eba4cb7", size = 557666, upload-time = "2026-09-08T13:26:06.511Z" }, - { url = "https://files.pythonhosted.org/packages/da/b5/aae34a85fd138c084440a0cc510cb245c11e5696f79e54f1a36225aae3b4/uuid_utils-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9ab40d7cfbbae6b2f291e597a664d82b8aee38debec9dac83c951adcf2c6c331", size = 287174, upload-time = "2026-09-08T13:26:07.968Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f5/0df3e19cb56969514d14b466069bb6c14f10f8d711f1b67487f280f34c6a/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1c12ee0c50756a35047fcaa7faba50b464462e2eaae65d60b608288a246191e", size = 322917, upload-time = "2026-09-08T13:26:09.442Z" }, - { url = "https://files.pythonhosted.org/packages/14/77/07b9c92a711c69c0fb6bc25b9b7f9de2fce00dde7d56e433b5475cceed1a/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab2b2b41690c6207ffafb01d0bc32ef7b3da56342b2b861ff5e03a772e39a928", size = 331363, upload-time = "2026-09-08T13:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/f28c80de9657aeb2fcd42ac0e5409f9dc22b3dd6438708e083bcf41c3a1d/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5f9fe3dfd076adc4460d3ede28c9225d47f998f77af77e67cb9d1c1ca935a0", size = 443183, upload-time = "2026-09-08T13:26:12.432Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/49ad8656c0c0565e17caacf3cf0d270605ca98775a957909d60daf7754d3/uuid_utils-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:184b79b46e85c322b537b48d81b750952f6874db8e0662441b176de13b10b0bd", size = 324259, upload-time = "2026-09-08T13:26:13.989Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/a88215e7fcb395929d09045c9fea2505ef0af7130aad2674a6d03543b142/uuid_utils-1.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f0bcbaea1199ddc92cb94a0d7df151e4fbed558e6c00d900a214913f67a901f", size = 346937, upload-time = "2026-09-08T13:26:15.555Z" }, - { url = "https://files.pythonhosted.org/packages/1d/19/7f07c428461fb3923081065a6d38cb6782c5e08e87ee28cb84a61eaf2797/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4c4bf45ffb105c5a8d701bcbe4c1a0a28a55d29090fc6fcb0d1504cb9bd2b85", size = 501465, upload-time = "2026-09-08T13:26:17.038Z" }, - { url = "https://files.pythonhosted.org/packages/86/cd/72c265eb24499b9b57bf368a349a57d6ff0b8a979b879ed7828a41714df4/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:36eb692a959e54815edb0df9b1f566fcf889292f92e810a1019d2fee783ba830", size = 607865, upload-time = "2026-09-08T13:26:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f4/2d698113ecfd9b071191219fcfb98852d924dcf5387a4b00458863547a58/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71bd49db6f19d9dff7dfd560c81b72083455ab22ce2114837fca5b3adb2b790b", size = 564249, upload-time = "2026-09-08T13:26:20.01Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b7/399e029f06cd587a5719ff3dd59af7a34124ccb6a5f6078304391b6c015c/uuid_utils-1.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d1890c89b50a70e3651db7a88aceba2f73c4a00d4bdca6c3c0c777480f69fefc", size = 528943, upload-time = "2026-09-08T13:26:21.579Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cc/6aa21ec6d99ff3eaa53673e23d06917ddfc1812a0361805ecf081ed9710d/uuid_utils-1.0.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6d94d66a073d76dbb3662baa1a659f4b6f4c87cddb9e5ced611e93a4fd34f55e", size = 99237, upload-time = "2026-09-08T13:26:22.97Z" }, - { url = "https://files.pythonhosted.org/packages/d7/c7/c6ba25b7153c4257c7629e7eab446830cc9b72c73772e687253b00d23e95/uuid_utils-1.0.0-cp313-cp313-win32.whl", hash = "sha256:a33de2ae30c8f5a0b82294ea979f19951c01f39a7800c9806b50d7a1b301253c", size = 170584, upload-time = "2026-09-08T13:26:24.495Z" }, - { url = "https://files.pythonhosted.org/packages/e9/67/c9815dce0216be38b0eb89fd1bf198657649422679b28d93697ba477bbb6/uuid_utils-1.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:3474e58925c9779012318785a82a0e883b5f99cbf1cde75f60c28b9252a3840a", size = 176648, upload-time = "2026-09-08T13:26:26.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/55c869c409c9b372d5e8a9ed709c4937f3030df34833644a599db0e70d91/uuid_utils-1.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:b8f6a3a66703943f5cfcd317b77565fe9b7c5d8eeee50282de673bdeff1697f6", size = 174877, upload-time = "2026-09-08T13:26:27.515Z" }, - { url = "https://files.pythonhosted.org/packages/9e/fd/6dfd6641e312d8d714c64ce19540b95d79caaa79bec31b41f35689bcd921/uuid_utils-1.0.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1501e0aef7e2ea759b6aa3967395896d803e8073d360ef95f62e18c52c5730f4", size = 562333, upload-time = "2026-09-08T13:26:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/71/ee/01330e815a75a5fe65f156ef194f2aabc214d3d8129e62c814579bde3040/uuid_utils-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:06f5da96427cfb28dc374a212ffe2d9becf5d099be9ee6b06600f77c31acb016", size = 289324, upload-time = "2026-09-08T13:26:31.069Z" }, - { url = "https://files.pythonhosted.org/packages/34/d2/0a5b7baba5590460c610f436c9108e5897010d745de423246192a5c5f2c5/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:874b4fbb841197d94f256db3058ede8687e5293f3838b988346aad1f9af46b12", size = 324645, upload-time = "2026-09-08T13:26:32.546Z" }, - { url = "https://files.pythonhosted.org/packages/39/4b/141d0547f40f1a56888e186722431971b2978ab82f001445d2aca8c0e293/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570de62607ca78bfb5f5ae09871aaeb7d5d3f41b625663df124fbd1f56a87a98", size = 334452, upload-time = "2026-09-08T13:26:34.337Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ea/342fbc8bcbf07cc3c137a01d6a4ddbe622b476120bb7c2a4be5c1699ecdf/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b87289c3e9e1ce8a6849d8754cbbbe06b96cade7b72261675eb0d5444955d935", size = 448997, upload-time = "2026-09-08T13:26:35.87Z" }, - { url = "https://files.pythonhosted.org/packages/88/da/6451810f642abeeb158b7db39104d55a6df729fa9068ab946dca72c9a7dc/uuid_utils-1.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0538babadda38ce86196315a710b10c7928697e33cc4ad01573aecadad9043c7", size = 326083, upload-time = "2026-09-08T13:26:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/96d2a6b9b6dd3d8ec52d3f00c21aa3ce94c7775d4cd1796655393b996d85/uuid_utils-1.0.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0482b3f41f9c9f5c2a59f865de8a9498e0a8d82649955a2b5ec5ceb2793ddb1f", size = 350628, upload-time = "2026-09-08T13:26:39.022Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2d/f0ab13421101c1917a3a969d16aa5b998643446a8c0a5376b2909bc81d50/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8af4a4166e84be69ba78851bcfb529302845f0551d2be87499e6f7c8859f9bc8", size = 503168, upload-time = "2026-09-08T13:26:40.518Z" }, - { url = "https://files.pythonhosted.org/packages/08/fa/1a31c44665623562def4b1991c7e2951ba3fa7678390d117aafdac45eb90/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9ddba367907dbb0892583dfd6a5989dda9257428b4022c86def98cc164db6096", size = 609820, upload-time = "2026-09-08T13:26:42.32Z" }, - { url = "https://files.pythonhosted.org/packages/df/c3/a441d1ade251b19b31728fe44aaba22d6d135dd7b90e4b6318d701ee62ae/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0e2e87a57cf1346786cd8067c1ebee1a0991996686a8fd9fa62db0d377a219ce", size = 567491, upload-time = "2026-09-08T13:26:44.309Z" }, - { url = "https://files.pythonhosted.org/packages/83/33/39a1d3a4d7e223aed19d61aeb3110d27eef2a640dcee0ab538f6b1821045/uuid_utils-1.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d892757ce2dbe4224a4fe5e589b9d12cefc77765c8d388b8f041433edec63d4", size = 531390, upload-time = "2026-09-08T13:26:45.886Z" }, - { url = "https://files.pythonhosted.org/packages/c7/01/f9139035e3fdbd9e395c23c60b0b5d97462930130f5bac68e4a4c1cf378f/uuid_utils-1.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:72db71871915b8048e63444c91587a28c92eb9ac15dba94568bef20ec350e238", size = 101139, upload-time = "2026-09-08T13:26:47.504Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a3/d914af7a3504a086eeb525625ead867b47731ce28e1cffbd995a6f923a51/uuid_utils-1.0.0-cp314-cp314-win32.whl", hash = "sha256:fda1280fdbc110b7e9166796e30974f3400bac8e1fe135c9da00e96acc7c51f5", size = 172895, upload-time = "2026-09-08T13:26:48.959Z" }, - { url = "https://files.pythonhosted.org/packages/cd/18/6c1700d7d637a4fea48f83b83ecd395bd66e685e2d6afdaf113be115f552/uuid_utils-1.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:7126a2b7a43ae6abdb5143aa228ebfefb8e436cc4bcbf91fbf080cf06c26f5cd", size = 178287, upload-time = "2026-09-08T13:26:50.325Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/02cd47c857bce282434b02732c5ee601e76a2c15db0590fbb130c2a8c2c4/uuid_utils-1.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:ac789644b2b50a5fb4c670df304f03259d9c2bec7c3b46facbfed760cd0249a3", size = 176454, upload-time = "2026-09-08T13:26:51.776Z" }, - { url = "https://files.pythonhosted.org/packages/a6/bb/805a581bac06982b94ce786092cdd48e7aaa3b4d02bacee7980129933979/uuid_utils-1.0.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7acf1911189491b976c0a55889c8420cf4040c242a3c3d46ce2bf7e654a2398e", size = 563300, upload-time = "2026-09-08T13:26:53.335Z" }, - { url = "https://files.pythonhosted.org/packages/b2/36/d5d54eb9b8673a0e410bca3e27cacfa153d6547635c13ac08e4625ed4a44/uuid_utils-1.0.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c2c3ca406fee6bae70aaed1341ce22d3dc50b34ab2d965174dbd3a69c9e5b770", size = 290189, upload-time = "2026-09-08T13:26:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/44/b1/2cb5fafc86d6dab4269e9bbebb125e204bc3f5d1003ef48741d87f97044f/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2852ae83fd158fbb7b842bdd5119010a3d101b3fc1d80bd6f2353469e3b054", size = 324986, upload-time = "2026-09-08T13:26:56.324Z" }, - { url = "https://files.pythonhosted.org/packages/05/02/1f9632c4c8ba04c00f41323e68221757abb3af32d8da706d2191001d212a/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b6c5661b63ca6b83c5bb4d916b00620883334ee639f61065ef9a6036e4f406f", size = 332704, upload-time = "2026-09-08T13:26:58.005Z" }, - { url = "https://files.pythonhosted.org/packages/c7/9b/6682303842806da6874022e706325601e86780dc4cc421d2a3ab1c4bb98b/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73facd346472b56ffec54d6dc05a0506d7ece0e7d9c6bb53e6bf711cab7e7f07", size = 448017, upload-time = "2026-09-08T13:26:59.616Z" }, - { url = "https://files.pythonhosted.org/packages/44/ca/4213f7bd913695b18cf6a280204368cc4869aa89eb6005b93d2a0013fe09/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad58a78d06c232f1c817f31d4b974ec59608faeffa4b07b859fc023f8add1d17", size = 327722, upload-time = "2026-09-08T13:27:01.078Z" }, - { url = "https://files.pythonhosted.org/packages/26/a8/691f91c8d28be9bc567983bdef53c49021ddfd3582fbdc4f9a3682895ac2/uuid_utils-1.0.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a1417bb6abce4039a1b4a8a4d83e6ddaa694809297f2d167bffea077cd7e8aa", size = 350181, upload-time = "2026-09-08T13:27:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/88/50/96143792454351baae7d5eafe877c67c9a3c1d9f5453ffd8a817f66e0de7/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d9c6ffb566f29d741c6e59a367d4ddf073fbd3decd50c43ef99aa3a3285b691", size = 503063, upload-time = "2026-09-08T13:27:04.232Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4d/333c251382484bcbc41e83046ecc95d7e6ece767cabeaf79af69d779f23f/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d826362cacccd6ca52140877c6b84fb0ea6544b9cdf8bd860eb877f76f16ba4d", size = 608178, upload-time = "2026-09-08T13:27:05.996Z" }, - { url = "https://files.pythonhosted.org/packages/69/db/57f42643e324d1dd4b8764e9f51f63cccc97e0ea9dfdfae4763e616e43fd/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:87f68b983e73ef8aacb4fbcfabda37d2d6e0431c54a209c1853bc3487e3237cb", size = 566908, upload-time = "2026-09-08T13:27:07.676Z" }, - { url = "https://files.pythonhosted.org/packages/c5/54/deb1dfd1634df28de05d73ee874fc8cc2745ba8b71776bddea039f6954bd/uuid_utils-1.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f18787481b251b700bce778a8906b7683478fd4895878ec4b38e150d481fbaab", size = 532506, upload-time = "2026-09-08T13:27:09.425Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/0e811a8817a1bb4e974b42ffbeac3eb8a01b6c75795c5632d8f078b69781/uuid_utils-1.0.0-cp314-cp314t-win32.whl", hash = "sha256:7217beaa4650030bc225d21583fe6105dbe33271b8cb994bca367cdc32dac47e", size = 172683, upload-time = "2026-09-08T13:27:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/fb2336e98432cc13bf26765740bef8d15951e62e3e83e103ebd081748923/uuid_utils-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a8cc8dbdda2615d4aef270bb0f1a931071b4211a21e18dcbe734143beab1780", size = 178478, upload-time = "2026-09-08T13:27:12.361Z" }, - { url = "https://files.pythonhosted.org/packages/ad/47/cdc26ca2af2fec7ecd7154d7a611237a56d39bc58ba8562f6cd0800f3324/uuid_utils-1.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b4920f5abfe2c84c5b2fc3e6b6063f8c7032289adb92d578750e0c1937ee77da", size = 176067, upload-time = "2026-09-08T13:27:14.183Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fc/83787071a199470a2269558b6874485a2fd4f2ed845232ef99bd1d08a0ee/uuid_utils-1.0.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:460c624577d490343df0f35c14bb4871f41738d0584c3afba91f2a8eded8dbe1", size = 567122, upload-time = "2026-09-08T13:27:15.896Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9b/356e7e17851693268303c7309620d24b569fe7dce1d14b1663c6f17e2bd1/uuid_utils-1.0.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:288690101bb71d79f2d5ca236ffe96306d5c131175e3af21370c0243520c9cf5", size = 289245, upload-time = "2026-09-08T13:27:17.446Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b7/98df02e50afbae0bd076386e8ec412824da87274d1a667d82d9abec9bdfb/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5124d050465317c795befc6340210239b33cea034d55271e7a8d6b53e56817b0", size = 328652, upload-time = "2026-09-08T13:27:19.107Z" }, - { url = "https://files.pythonhosted.org/packages/22/f4/0ed779f46b5b52e0fd4ee5d8168bb81853cecf03f108e8b1f5cc8e1d57be/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0938813da8301296bd34bd707cec47bb718f90f9f31aebbef607ebef0f3d5da", size = 337027, upload-time = "2026-09-08T13:27:20.947Z" }, - { url = "https://files.pythonhosted.org/packages/56/70/29df489a5736544dd8583ee8e84e18b9051f1c04f8b83535727abd0473ae/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed2a8b86e77bc33a46c54345bb8648c4c6773cce9e20b6cd9e629a8f482718c5", size = 451780, upload-time = "2026-09-08T13:27:22.469Z" }, - { url = "https://files.pythonhosted.org/packages/ee/05/ce99a3008ed2678c25c45137053869fccfd83fdd54976fd1defc6265485f/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7e78528fdba973fa37110221bdc01df0bce78807496c0898dc384555ba3c9c4", size = 329155, upload-time = "2026-09-08T13:27:24.017Z" }, - { url = "https://files.pythonhosted.org/packages/31/01/86ca857663951ca4287390047e27dde24e248de3b16007b03dd088b719be/uuid_utils-1.0.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90c14789ce9e04a03111cfec963d1b5f988a665573b1d94b6fcfe325ea2c3fbe", size = 355746, upload-time = "2026-09-08T13:27:25.573Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d9/612687380487212a891ff369ba64afda76c7ec64d39d7f2eea28f2b36737/uuid_utils-1.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c59fc1a184b34e00a60ed77e8d2913a1e7f197af912206000f94b53e79d1f64e", size = 181648, upload-time = "2026-09-08T13:27:26.987Z" }, +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/01/baff7ef80f30885cca5ecfc0d9ea660b3d20c34dfaf0d17a79a95ec342d7/uuid_utils-1.2.0.tar.gz", hash = "sha256:8a8bd1120a8bc5a6656a1c580d66a050af430b05d017da526ec0b0a06715bf70", size = 43765, upload-time = "2026-10-26T11:08:50.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/12/ea68ace50014a047a1fb08b66d9458e48ef132a36b9664101365fe262337/uuid_utils-1.2.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:325432c5db7cdf0fc94fe30265ffd8c8705d468b922e02d8d92b4bef91ab99ce", size = 576108, upload-time = "2026-10-26T11:07:31.114Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/7e1cabaccad7bf6d4fabac10dab493c3ea72de6bd241331972ffaa692f21/uuid_utils-1.2.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3ca80839df75b10b9d89a3ca9193e6df5500095c8efc80e5286d20ccc03746ac", size = 295123, upload-time = "2026-10-26T11:07:32.796Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/00c19434487c8066eda5902a8456737f096d3fb9905bff8dd496afc80043/uuid_utils-1.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7637fe5b6c4bb77a3a973bc0abae32b566ef8415c92cebcc91492b186350fbb1", size = 329552, upload-time = "2026-10-26T11:07:34.281Z" }, + { url = "https://files.pythonhosted.org/packages/fc/72/ad0f4ce574b3535d22ceccdeb3e2bce165276319e52777a186b73f2de059/uuid_utils-1.2.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37359f40ad4a143b80a71887a6e60d9c6d7841915585fcab04be7f0fe1bbc5af", size = 337553, upload-time = "2026-10-26T11:07:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/26/51/59536f85f57870d4a5b02faadfd326e0a0b32b9ea2f8a8a4026868a21012/uuid_utils-1.2.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64f4e98e1220a1323db656535309385fbcd9bd0e65a4c43d1fcc41b4b5b2e8b2", size = 442496, upload-time = "2026-10-26T11:07:37.389Z" }, + { url = "https://files.pythonhosted.org/packages/00/23/2b68f9ffa7da1b344140b4f013fc56270e1fe43310da815104e6db886e58/uuid_utils-1.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a1ed18b454295be2a8990bea89b3385238356ff6790e4fc256731986f413b87", size = 331264, upload-time = "2026-10-26T11:07:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/8990fdf8bdc9d189f2ce30de0522ad085e2f3d4e5335044000e6199959a9/uuid_utils-1.2.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:180040a8f38c588bd50c0178736ddbb8c18cfd25d3dc4ed19ea6ede45ee8059c", size = 356084, upload-time = "2026-10-26T11:07:40.534Z" }, + { url = "https://files.pythonhosted.org/packages/9c/09/5de2448d6e7b113c0471055491c21841a94680811c571270ca2d89b6e3b1/uuid_utils-1.2.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7b3ba967ac955414ca0ee1231de0b9693b6c454d72cbf43da05812bf18d71c7d", size = 505643, upload-time = "2026-10-26T11:07:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/fe68de666a3148b57e3736b04eb1997ceeb0b82cec4eb657676f393e1116/uuid_utils-1.2.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:97134fdd3cf32f5432f8249630d2b7059caf8ac815e3316a05d2a1b7e1704144", size = 612085, upload-time = "2026-10-26T11:07:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/e6/11/26b3c9dc8224e87e1c0581a190d130239f3a798287ad791dddbfd1437392/uuid_utils-1.2.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:9939d3a0e94a34b5d3ff81293eff6d2fddc7704486ddb11e3d6251152fe1be31", size = 572063, upload-time = "2026-10-26T11:07:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b2/29f5db24b4b0cb2c4e9e1472ae5969f9263c558e6abcbb6032334f3db978/uuid_utils-1.2.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:22bb149628ef34837c95f718657ad227221e2e8729267b99ae942ac6af22ab6e", size = 536932, upload-time = "2026-10-26T11:07:46.654Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/fe1806c104a374158511647b0ba8cc2449a9629bf6925128f82e2b3266af/uuid_utils-1.2.0-cp310-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a1b6725a9a0368f55f819e145b0f943e5491f7d5c4cb6089d62b240f4e3baab9", size = 104061, upload-time = "2026-10-26T11:07:48.153Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/933b236fcb84c80d8095c31bd2a71528468ef8876c6749084045b81a1bfc/uuid_utils-1.2.0-cp310-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:10bf3925327dbc77a6bc4750bf6e117661335de3322df59a96d5c6118442e460", size = 103999, upload-time = "2026-10-26T11:07:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/8b/03/e036bb6b577e25182bb1361a4e627f60e79cea48251d40d30d9952f11f29/uuid_utils-1.2.0-cp310-abi3-win32.whl", hash = "sha256:816b02212e09ddee97b42bb3e6f5926b238a46d49ef18d907062468d3ec56faa", size = 177530, upload-time = "2026-10-26T11:07:51.122Z" }, + { url = "https://files.pythonhosted.org/packages/01/26/4698de01c11b3fcf784e847f5245f52224cea4db37cbfa0a08ee76a1d38e/uuid_utils-1.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:5d66aee34594f9bda7bfcb68f7db7ba9b3a89d5f221f8d42a8f538737ae1164d", size = 184093, upload-time = "2026-10-26T11:07:52.455Z" }, + { url = "https://files.pythonhosted.org/packages/ca/52/8ddd5a08f97ea254a1d2241dd7c2e6a826fd4010ae7cc35bfa3b5c9db94f/uuid_utils-1.2.0-cp310-abi3-win_arm64.whl", hash = "sha256:2819a38f3f0bb232edbc3ab134551bbefd6a5d6ec59084acdc1a6572f231cab1", size = 182004, upload-time = "2026-10-26T11:07:53.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/7a/7462b83f5fe77429bffbb38f314b09d57730e54156c13a73a1cbc39aecd4/uuid_utils-1.2.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e011e1b992eca52110f8489b82530a59ad8d97439e4483ed65e125f147959eba", size = 567239, upload-time = "2026-10-26T11:07:55.539Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ae/f5ceb473d75080dc461914a58de790f1e8aff0c6b3f355fa1e8c837b6bdc/uuid_utils-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:806e1e37e77d95406699ac154ece22bedd2eeda41f5a5d45ab7c3b2f79bf9285", size = 292729, upload-time = "2026-10-26T11:07:57.128Z" }, + { url = "https://files.pythonhosted.org/packages/54/af/18acf6463802b403d1faeb8319a58e7daaec67fe136eb44979b69fbfa77a/uuid_utils-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96b5e52ca1722ae0d3f03effd4b8063f0725a7cc9f760954e0f1c36ab52e9444", size = 324560, upload-time = "2026-10-26T11:07:58.684Z" }, + { url = "https://files.pythonhosted.org/packages/14/24/f0adc745da334caba87a5041bb1fe598347d8eaf35de89aa21b14fc57ab5/uuid_utils-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d55790277e8f9f6cba68bdff2be701a68987b1622cde56e50317c366fb8ae994", size = 329667, upload-time = "2026-10-26T11:08:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f6/a45af3098af0e43c811212d8f2634f6b5959adfedd807e6086cdc2d54836/uuid_utils-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71743b880787cf97934d609fe4561022bfb22713dd1e356622da3e7c06e348b9", size = 436256, upload-time = "2026-10-26T11:08:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7f/1ca1cca384d65d5533fa2fe8bec9e5f6bc4e5de634582eb3b5a62e6dab33/uuid_utils-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf993b4a92b0edd64f7ef46799d5ab07755c84781f2e5700a49a112f1dc1092", size = 326366, upload-time = "2026-10-26T11:08:03.104Z" }, + { url = "https://files.pythonhosted.org/packages/e1/59/537100eaf21796237afee5523823883e991b69b157e9a1ba224ac9d60298/uuid_utils-1.2.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:83af4a16175aa98f19e661d3eb9c210f4441723cdf477b959d7015111b653a54", size = 347030, upload-time = "2026-10-26T11:08:04.794Z" }, + { url = "https://files.pythonhosted.org/packages/0d/84/de0aac9162713c233cbc0cdcd5198e2e342373eab3c18743fbc71c4e5283/uuid_utils-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6dedd223c46187017a02d64df9734c78fe5460b6a6e21912b48abcf6388f2934", size = 502312, upload-time = "2026-10-26T11:08:06.234Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9d/fb3624fd335d97e1a99be8498b4ec6784f35ce6f5f9f6d96e9e12467afea/uuid_utils-1.2.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79cd62145b2575c08946c9f3dafed7418ab590bb847b50d8c60acd109e0bbd6c", size = 605106, upload-time = "2026-10-26T11:08:07.847Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/c5e80c63ae7330c541312c282b879da8a89e269cf91a9923771aa22e52d8/uuid_utils-1.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5632d40fadae4a0856d5f6bc30612833b807589e8c804e3d479110fb284493ff", size = 564964, upload-time = "2026-10-26T11:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/b977028fe182e797f196dd0f0a9fd62ff2a2c21a61011bcf9009f6137e59/uuid_utils-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:555933b63e520aa52d092aef7a0c43577da1879568b35954a977f86f5eb741cb", size = 531699, upload-time = "2026-10-26T11:08:10.955Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4a/676338df4204eb88658a316b7a492fbce9866d0dc5f85f573197505886b3/uuid_utils-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:1941c938ed88406b3866361c0069adb712b8b54782600f6416f759d30ab09ad7", size = 173619, upload-time = "2026-10-26T11:08:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/1c76cd5ab1a7c4327bbd1e7e26c42d8f4cc3ab1267691f5f373c26cdd653/uuid_utils-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0610e92b6b8b4753803c1828e2341627d000c63cdf575259a25b2346a7a8545c", size = 179326, upload-time = "2026-10-26T11:08:14.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1a/bc306695a03dcfa5f1584dc8dd43b7c74ac991a654cce21a435f45603fb9/uuid_utils-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c55216f949fffbf684c6f8f8ea4c882f6dc6766c81a6a5cb42a0e7ca09c4b234", size = 177336, upload-time = "2026-10-26T11:08:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/03/a0/3f202d555461635c879c032cfd26990d78b5cfd7b4273409f94ee06d4d59/uuid_utils-1.2.0-cp315-cp315t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c7d3749a071889be8610f02fd4bbbfa1eb24f5d7f32b6ae1fb653c1b4c09a39e", size = 568287, upload-time = "2026-10-26T11:08:17.245Z" }, + { url = "https://files.pythonhosted.org/packages/e5/89/239c462fd5ee61b56f2c7d18af093a5f5a0cde09194286f694797cb3e85d/uuid_utils-1.2.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4532e404f60e2ebe5d75104ae7d1f79f01cda6b0ba169170b11c02446012395a", size = 293016, upload-time = "2026-10-26T11:08:18.86Z" }, + { url = "https://files.pythonhosted.org/packages/43/63/56d0ed7386ffea5e25a98abdf88f01f1f933530ad24726010396956a0edf/uuid_utils-1.2.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bafad0526ceb1901252f38509609ee6e8b7cd358e7b5eed4e3cd30dcb2d2f41", size = 324511, upload-time = "2026-10-26T11:08:20.441Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/b7b6859d338ea36014d0b77b9de41fa803757040db7598cd278d9f4e6586/uuid_utils-1.2.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1d327972b03f44d5c5f25bc479ed6b3672eb749d56d78738bbdb8b849df684a", size = 330148, upload-time = "2026-10-26T11:08:22.182Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/f689bae190a2b04d0f3c7cab7cf040e02f19729fddaa2acbeef67a13e397/uuid_utils-1.2.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89983195942a3d8469df4ef79a4ad1f5d2f5ea26c518206daae8c42a85ff781a", size = 435497, upload-time = "2026-10-26T11:08:23.671Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e2/279f01c7fe040df3e5d6329abd4f2d31570f612d79b4003c8edaec9a2129/uuid_utils-1.2.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdafb0f430718f1fb56695e3a5b6bec06c5305797027653d568135be89f3109", size = 326704, upload-time = "2026-10-26T11:08:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/61653bb35b673d5509838c804656a00f3fae17c1869d35ab000183bf136b/uuid_utils-1.2.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:71fe514ba633707496d7ba21e2ecf87eb3c54787094eddc36227e661ad697823", size = 347816, upload-time = "2026-10-26T11:08:26.554Z" }, + { url = "https://files.pythonhosted.org/packages/50/21/48a7ef79de05bc343cc3f5b1ce5fd27036a04a1e7bf6366427623a5e91bd/uuid_utils-1.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:605f05c783cf9a18dea7cb65fd6028748de7eab355979988af034e93c035486e", size = 502417, upload-time = "2026-10-26T11:08:28.407Z" }, + { url = "https://files.pythonhosted.org/packages/63/38/44838b437142cd443be9f51c4871f2075ace06bae8916890285327972142/uuid_utils-1.2.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:424b7bc4d0f59b8a5028db97b44ca4693c08da2aeb1a83f27742804a9f2c43eb", size = 605752, upload-time = "2026-10-26T11:08:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/60/6e/29adbf56b65c2c93b9bab6316ca234fb7ced751aa64352edd5d35cb29eac/uuid_utils-1.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:e1fa69a90aa1c722a778f08d9c358cabbd2be6ae9c000c300c93540ed3455f74", size = 565349, upload-time = "2026-10-26T11:08:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/77/31/c75755cbfc4955ef019c6eb16feefff22139c59b9a4093d51ae75704847d/uuid_utils-1.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0a49728a9b9242bb6e0298b1cae6ab132befa88115d98b492e0ac68b9db41dba", size = 531585, upload-time = "2026-10-26T11:08:33.071Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7d/a8e91b157787b4ed4b498a7ed09d18baa8fc4e6b1828662db188b7f3276e/uuid_utils-1.2.0-cp315-cp315t-win32.whl", hash = "sha256:c52cfc3388069a6dc787d9100815d298a9d8be8c37835e4d039af5e3cc048409", size = 174033, upload-time = "2026-10-26T11:08:34.49Z" }, + { url = "https://files.pythonhosted.org/packages/31/a0/35786696cf1abb7fdb2711e2421bdf650b075eee1a971040d62148a0f0a2/uuid_utils-1.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:0ccace291105dadedb78bba2383ff49ee51138473507eda3ec274fb654451916", size = 179593, upload-time = "2026-10-26T11:08:35.896Z" }, + { url = "https://files.pythonhosted.org/packages/65/d4/e3f6ab681754dd0181c61193b8f94c77e0b798a0d181feccb928a5a43155/uuid_utils-1.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:ef4b07203bff2511deba54e36589141aed99a7a4831d7f73a3e357dade624bd3", size = 177283, upload-time = "2026-10-26T11:08:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/02/c7/1e655b92c3cb35103d1f38a4b113a65393277208297587b33450d984ed06/uuid_utils-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5987ef57e4c0bd5284a374abea49f1bf2f80e145ff2ad8d7f1a97590788ae86c", size = 571810, upload-time = "2026-10-26T11:08:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/e08716f0c276e73045a81c68bfd89cc593fedd963c5b8a45ebf93a62b7d9/uuid_utils-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a11c7eaf24bd4efaa2d50c81ec373b2673c3e8fc700d8b1538e5278966f34c7f", size = 292508, upload-time = "2026-10-26T11:08:39.986Z" }, + { url = "https://files.pythonhosted.org/packages/d4/82/e97b8b05c9d769eb4f4ced71d7f61e2a5ffceb501fcd14810ac9ab863fe2/uuid_utils-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94156c3383fa92b6171ae34fd16227445a41c1efee09ac532878445400c16e23", size = 327607, upload-time = "2026-10-26T11:08:41.515Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b7/7048c864d81ced8d893c2807a60c4d64f4c807d31f76819d60974c637e01/uuid_utils-1.2.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3c9a7ae482c019e791dfe19c97f6b46541a061de3faf4d1eefb58c4152b8ff", size = 334924, upload-time = "2026-10-26T11:08:43.074Z" }, + { url = "https://files.pythonhosted.org/packages/2d/0b/0218ef04fc2fcf2d848bfc752e421e0dfd5042a317f64f32ed1c8938820b/uuid_utils-1.2.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9617882972837dd99a4fd2826bd27a1de062389447455965a58db37db06d0721", size = 440247, upload-time = "2026-10-26T11:08:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/4a/66/b3d2ed0ef738efaec6ee3eb66162959a77d3859e2cc27a472657bd5563e4/uuid_utils-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3142fb087b67d95a005732469abd6f5032e51a5a93b23b2818e2c9624356ad1f", size = 329546, upload-time = "2026-10-26T11:08:46.199Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/b1be67ed5de6d19eb10d153c94b9320cce56c2df3fab62eed0a194cab702/uuid_utils-1.2.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1909c04f2e5c813c625873621aea3b3f2303a6bc7a6231da6deb69dc4df350d3", size = 354068, upload-time = "2026-10-26T11:08:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/c9/54/5eebf7cd2282f81df1db7864f3b76314ad85b603566927281659406807d1/uuid_utils-1.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:16d9e7b719ac3e018519664407edb5e583f343e8a8e3941979f26a02bdfc6c28", size = 182385, upload-time = "2026-10-26T11:08:49.222Z" }, ] [[package]] @@ -6993,95 +6681,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] -[[package]] -name = "watchfiles" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" }, - { url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" }, - { url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" }, - { url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" }, - { url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" }, - { url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" }, - { url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" }, - { url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" }, - { url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" }, - { url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" }, - { url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" }, - { url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" }, - { url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" }, - { url = "https://files.pythonhosted.org/packages/cd/eb/289fec71eba51d6c3ef3a28c81f7213f4e860084ffae5abc866933c564f0/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1", size = 396932, upload-time = "2026-09-21T09:08:50.217Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/a3c3f557992a20a5bb24607f0da44d39cf6d6ba3f8f6ed7cb31f6c73371a/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c", size = 394852, upload-time = "2026-09-21T09:08:51.412Z" }, - { url = "https://files.pythonhosted.org/packages/55/65/d45763a5e1108a65af85a217af2930ebf1081c73fd8169d3f75e586992bb/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef", size = 451789, upload-time = "2026-09-21T09:08:53.144Z" }, - { url = "https://files.pythonhosted.org/packages/02/96/165c6e540bf765ca1115062017adf861d0816525f02464ba71313fe7cf92/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f", size = 456919, upload-time = "2026-09-21T09:08:54.372Z" }, -] - [[package]] name = "wcmatch" -version = "11.1" +version = "11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/cd/0e028d8b7d906d51acdac537334fbe85f8284f2a8147d48bc9403af77a0d/wcmatch-11.1.tar.gz", hash = "sha256:f2b6f0fad6f5e2571498723fcb3c8facdd0fba0dcc139f31d32f592eb648153e", size = 129091, upload-time = "2026-10-10T13:10:54.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/07/b7a9162a804bc0f7d03fe84c93e374d3e1c4d828043d5f9b797f7d4e30e0/wcmatch-11.2.tar.gz", hash = "sha256:1605bde546fbde4c063628c7d89eda486909fd4337e51d73a873987fbcb2554c", size = 134939, upload-time = "2026-10-22T00:37:57.622Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/22/c4adda5a3754b7fc9429a544a0673fa6922fdd7808eab09abe18616f6e38/wcmatch-11.1-py3-none-any.whl", hash = "sha256:2863afee97a7dd62e64f7feada864848c1a5e31cb331b3aa530fa1300d2d5d3c", size = 43417, upload-time = "2026-10-10T13:10:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/34/96/b6cebd4bd75320743a291fed494c79f07c01b58fa9b187d12aeb5110f002/wcmatch-11.2-py3-none-any.whl", hash = "sha256:952bab83c75c233836007a69550a3a8753cf06964c1eb3b84ff11ad5bff1c272", size = 44361, upload-time = "2026-10-22T00:37:56.293Z" }, ] [[package]] name = "wcwidth" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz", hash = "sha256:ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b", size = 955039, upload-time = "2026-10-05T00:24:05.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/1e/4532a81fb9dfbf4114a816775e0a36c3a64ee1d1f4bba2094e2da50be5dc/wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07", size = 605343, upload-time = "2026-10-05T00:23:22.649Z" }, - { url = "https://files.pythonhosted.org/packages/a0/07/cb6940e81134b7ed25fa312ee9ab536a63db0793b149f88a90e603ceace9/wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17", size = 609960, upload-time = "2026-10-05T00:23:27.049Z" }, - { url = "https://files.pythonhosted.org/packages/a4/80/15ad05d40bfa99155639fb9e13b3d77083aa0fab893c816db2543d29005c/wcwidth-0.9.2-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79", size = 762566, upload-time = "2026-10-05T00:23:38.322Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/b8ef7758003d66b60f093695831a86dcc726aac01ee6446ffcbda27b61e3/wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724", size = 771632, upload-time = "2026-10-05T00:23:32.448Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389", size = 769763, upload-time = "2026-10-05T00:23:40.175Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/285f862826f721964ec7c42f81dc53d23afbd723a0f4cd989651f8218e25/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7", size = 782134, upload-time = "2026-10-05T00:23:33.926Z" }, - { url = "https://files.pythonhosted.org/packages/c2/2d/64aa54882a5d556d3654c1f926d9118b797461033e23a158409941a37c8f/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2", size = 782851, upload-time = "2026-10-05T00:23:41.974Z" }, - { url = "https://files.pythonhosted.org/packages/59/39/52389f6de7fe2e9c14ceb8253dd99034bd86e1c87847ea3c100a97dded9a/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04", size = 784621, upload-time = "2026-10-05T00:23:43.449Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/20225500a076ace27bbcc8a6fd7c55125133c57a618816c7b7b8b73070b1/wcwidth-0.9.2-cp310-abi3-win32.whl", hash = "sha256:ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4", size = 593123, upload-time = "2026-10-05T00:23:55.953Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/b0690f55ea0483530a18bac917fbadbf54f35122510446fc370f5f1c2453/wcwidth-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec", size = 597827, upload-time = "2026-10-05T00:23:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/e5/11/6ecf4e9e268ab1a4ec617ffcccc2ee4a71301625f5490912dbaba462fa9c/wcwidth-0.9.2-cp310-abi3-win_arm64.whl", hash = "sha256:b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa", size = 598415, upload-time = "2026-10-05T00:23:51.517Z" }, - { url = "https://files.pythonhosted.org/packages/4e/41/549eef1ab767032bdbdc1f0ab655d404b082b1e9a1dab1361dbba90f64ed/wcwidth-0.9.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eda88ffdc97c0fbf193d407114f2c7a54b379f67f6e52a7531ee3b9fe749eca7", size = 606555, upload-time = "2026-10-05T00:23:24.188Z" }, - { url = "https://files.pythonhosted.org/packages/9b/64/a875ed7ea71cacadc0ae11b5fd3fac3486efd58bb25e67a7344248dceadd/wcwidth-0.9.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf361c8705576760623b4724ae564666d73b016f9a778bcfd1c7345378ef4ec", size = 610829, upload-time = "2026-10-05T00:23:28.563Z" }, - { url = "https://files.pythonhosted.org/packages/c6/98/513095e484fe79b6f2613d6a72f855f5d56b65e15c215c2a6746fbc638f5/wcwidth-0.9.2-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:97b878d1e158da5ed9ac5aac53fa3a55e282103af6a09ec353865613d1a31a76", size = 775819, upload-time = "2026-10-05T00:23:45.116Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/c02f3eec57224731e78f84b68e272250f784b6205acc7e0dcef6a7c23a0e/wcwidth-0.9.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:59dab4049cbd982b478bca098528df2c79a9160636a3a163ffebffcbd7d1b892", size = 786915, upload-time = "2026-10-05T00:23:35.323Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/b0529a79bac3fe8d94f32b4237a13dbc3f955508753f6a6f06c73d679dc2/wcwidth-0.9.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb08ceb501d6aaf94066c3ee122dd825b152df40ff0bd0df4dc27126233b948e", size = 784186, upload-time = "2026-10-05T00:23:46.366Z" }, - { url = "https://files.pythonhosted.org/packages/d5/bd/6357c84ca9a734bfc735b7c48dbe21336b3777fab8a4101d14976dfe49a7/wcwidth-0.9.2-cp314-cp314t-win32.whl", hash = "sha256:8b4e381590b9b7390e07e22b2c0c1bb96ce50e1d2243c866d9387600362d51ed", size = 600791, upload-time = "2026-10-05T00:23:59.398Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/037591ca18d897cc2179559dde72e6efc6ce0c90e9cd1e6bca4e87c38b4b/wcwidth-0.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2f7b3bba5a5d5f31fc350fd36ce5b84b693c83b7eb95ee630b720da5a5ce06f", size = 605463, upload-time = "2026-10-05T00:24:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/d0/07/c9d96e106d938d26f7ab639bc80b8199359a1645ba6e3498413313ab6f38/wcwidth-0.9.2-cp314-cp314t-win_arm64.whl", hash = "sha256:734aa9405b321d1042301aa19c943c4731ee9e3460e4f8feea3299c064c97a14", size = 605936, upload-time = "2026-10-05T00:23:52.765Z" }, - { url = "https://files.pythonhosted.org/packages/82/8a/a28d61d910005ac93dfe48be3a0ebaa49352d88cebd25323e69e6ff2f4a8/wcwidth-0.9.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724", size = 606575, upload-time = "2026-10-05T00:23:25.663Z" }, - { url = "https://files.pythonhosted.org/packages/01/c2/a3c66bd32766c8f4d6dc47d572532ba014fe5be30489f2576aff7cada363/wcwidth-0.9.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2", size = 611045, upload-time = "2026-10-05T00:23:30.421Z" }, - { url = "https://files.pythonhosted.org/packages/ec/8a/d39964f8f8c019d7d439b9b501d3e7bb42fee69f00354040ba0b27b5824c/wcwidth-0.9.2-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c", size = 778143, upload-time = "2026-10-05T00:23:47.7Z" }, - { url = "https://files.pythonhosted.org/packages/2f/53/525da13e8f9ff7b5b4e74ec6f8d68bdee63905796972e086c6b1b96670d2/wcwidth-0.9.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d", size = 786630, upload-time = "2026-10-05T00:23:36.967Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9f/d6a0c6df354b9d93466548a65cbf4ffcb48c719bbd307504cf3e76740837/wcwidth-0.9.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270", size = 784839, upload-time = "2026-10-05T00:23:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d7/3021feed1ed7926021ec134943ad3b24a2f7ea742cc9976461171482ed77/wcwidth-0.9.2-cp315-cp315t-win32.whl", hash = "sha256:0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b", size = 600845, upload-time = "2026-10-05T00:24:02.497Z" }, - { url = "https://files.pythonhosted.org/packages/63/80/6a03356d8ee38261e3a78cf89ee03d8e7f12c572d969237be00869e2dc73/wcwidth-0.9.2-cp315-cp315t-win_amd64.whl", hash = "sha256:33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9", size = 605441, upload-time = "2026-10-05T00:24:04.052Z" }, - { url = "https://files.pythonhosted.org/packages/0c/48/1a308a86a833fd12ff7a08d0d2491ff4a72c8a92d12f5ead8317630f771e/wcwidth-0.9.2-cp315-cp315t-win_arm64.whl", hash = "sha256:48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8", size = 605885, upload-time = "2026-10-05T00:23:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl", hash = "sha256:89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e", size = 301667, upload-time = "2026-10-05T00:23:21.097Z" }, +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/01/e5ac23733628a6bbb91913a765fcf55ee8070d8d991bc88fdaaf805052c7/wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954", size = 970051, upload-time = "2026-10-24T18:11:44.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/b8/d251589a100250b71a7b8e83fedfcabfd8bd60e78cd1e3329372f63bcc93/wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d", size = 612952, upload-time = "2026-10-24T18:10:55.925Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/dfdca30ca36c472de3e260406dee29fc108499ec590c63b3fe279887ee9d/wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4", size = 618904, upload-time = "2026-10-24T18:11:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/d7416f38054a107d2fb17c2b73b68723c6c334f69c4e29545fb9e6df81eb/wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09", size = 778780, upload-time = "2026-10-24T18:11:17.299Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5b/42bdf9a4b0e4f9126584af2ec73642de495f3bd8add907430f13c3bb98d2/wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef", size = 789012, upload-time = "2026-10-24T18:11:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e8/0485850748dfdba9443f4151c7b2442cfa4987ba6cd871038eb5d6b8eca9/wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2", size = 785946, upload-time = "2026-10-24T18:11:18.748Z" }, + { url = "https://files.pythonhosted.org/packages/e9/11/dd3d0379c6094524df6585b76ae3d9727161ff29c003177e3661c36437c1/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f", size = 795348, upload-time = "2026-10-24T18:11:09.931Z" }, + { url = "https://files.pythonhosted.org/packages/3c/48/1b5299983d9ec084be897162b7cc6014622dfe62847224fa3272b6d79ea5/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763", size = 793420, upload-time = "2026-10-24T18:11:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/7a/be/e3097d859b075344a7be57774a58d468eafb0ca88e02aad93eb31d69f6ba/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17", size = 796192, upload-time = "2026-10-24T18:11:22.552Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/739710c8cf0de461239db5bb6ee678c9a33d30aaa22beff55cc356781818/wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62", size = 593229, upload-time = "2026-10-24T18:11:35.609Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f1/c123f902bc2ff9e9c2ab66241b8872204df87cbb18ecdf035e120860fedc/wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c", size = 597780, upload-time = "2026-10-24T18:11:36.952Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/1cb5ddaf5510b918e01aa95fc4b68df91dead784b63e4931037c5673e85f/wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49", size = 598786, upload-time = "2026-10-24T18:11:30.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/46a31c12b4cdad04f546a3c0aca5d000d77aa5069ae88b4db24525cc29c4/wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f", size = 613805, upload-time = "2026-10-24T18:10:58.887Z" }, + { url = "https://files.pythonhosted.org/packages/18/f6/5a8327af9a65153ea9823d6a12fc182f325264050aa25c05a5deadc189c9/wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7", size = 619936, upload-time = "2026-10-24T18:11:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/bb8a7f4aa9b915e56e09841b07636ba49c82ec987ad3d2022e1696c038d9/wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5", size = 794563, upload-time = "2026-10-24T18:11:24.4Z" }, + { url = "https://files.pythonhosted.org/packages/12/0c/986acadef534688a57502d484a40e0f30bdca2d39514fe0a311f7d575f70/wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967", size = 806565, upload-time = "2026-10-24T18:11:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1d/66a94b9ac07037d7cf2c1f1288867c9932215d467893eaa5ec8d548c7f2f/wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633", size = 802372, upload-time = "2026-10-24T18:11:26.275Z" }, + { url = "https://files.pythonhosted.org/packages/d1/00/1d50b8e8b45a8330b582695bf8196081a609317928a3718cf21b3d14a899/wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3", size = 598917, upload-time = "2026-10-24T18:11:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/3e39a33fc8f5c4e07611578710275039c9d4a6655e2be7cf3459401a30cc/wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab", size = 603536, upload-time = "2026-10-24T18:11:39.922Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8c/b1f399fd2f8449e42978192967ab6df441d7cf894b10117983c36c062b18/wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95", size = 605127, upload-time = "2026-10-24T18:11:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/04/46/839780e772e9068580619d4d691348b7a5f9c4fa5ffe501e522eefeb27cc/wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664", size = 613850, upload-time = "2026-10-24T18:11:00.725Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/b2559c7012bbd4268f5b5c0288cb793c78235d354e306871a55893aa9d1a/wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279", size = 620207, upload-time = "2026-10-24T18:11:05.975Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/7d2c39e28924b2430b20099e36276f14e00e75c7bbe4ba5b1ed44d1d465a/wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed", size = 797056, upload-time = "2026-10-24T18:11:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/f4f9967a411e02b89b74df2f49f44977c4badccf29738f3ae6941619e27c/wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d", size = 806169, upload-time = "2026-10-24T18:11:15.724Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bb/e2b40c6522f3aa1222558487dda3551d297e07e245761b4816f6caeebc29/wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d", size = 803462, upload-time = "2026-10-24T18:11:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f2/ab69009f2dd0d3772d3e07f025fa046b33c60c195e4f2e5f000297d23547/wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27", size = 598931, upload-time = "2026-10-24T18:11:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/63/6f/b321f10c2298e7d16cffc1a5f05f3d7e5231b45f9e68af5ee378e23bb2e5/wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6", size = 603485, upload-time = "2026-10-24T18:11:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/4e/cc/7eeb28059186cae72a770df79e744f232ef068192a5a588b718473e35bb4/wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b", size = 605198, upload-time = "2026-10-24T18:11:33.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0c/309b1813b230d1dbbd6f2c2ab2bb21ef68992358977605d9ef289bbfedcc/wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d", size = 301806, upload-time = "2026-10-24T18:10:52.651Z" }, ] [[package]] @@ -7140,88 +6785,88 @@ wheels = [ [[package]] name = "wrapt" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/22/89e2f3bdae5cb34e0cab0cd86d7172dbf418de4b46c9b17b9c7a560dfa44/wrapt-2.5.1.tar.gz", hash = "sha256:f595bb0185aab3e9dc31950c95d914f56ea8278810c3b928f3426e12ed6d27bc", size = 184455, upload-time = "2026-10-14T00:39:39.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/2b/0f2ae9e355a0491c202a1331ec405d794c249f4fe9b4db952c0246909786/wrapt-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aed178902c2386d7c5d3d23eb96d32c100e34cb8c2390e7ece0e4901ae43f0e7", size = 105587, upload-time = "2026-10-14T00:37:01.614Z" }, - { url = "https://files.pythonhosted.org/packages/0a/54/a5b9904d341ae255bc5618ac43830ab68ed6420dac62af9a305a6191062a/wrapt-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1910be5adc0232cc6e8c0673bf3f41c2ee724547543526bed8d00734458e7bc5", size = 105759, upload-time = "2026-10-14T00:37:03.02Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bf/8edaec939d7411a58bccb4dc4310246caea5050576ff91b5f5abf6079a9c/wrapt-2.5.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c25c594f58ecb676358d6d6b0ff068b8bbbc506dc831c6d17876460c66ce39c2", size = 240110, upload-time = "2026-10-14T00:37:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8c/18ad7f24c82cbf689324521abe4dc90078d20a5498d991ec83e03dc60c85/wrapt-2.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e85a9db9e5a5ccc326edb19e35a5106ba16e451d570a2ec8ea9deb1ea52a3c42", size = 241618, upload-time = "2026-10-14T00:37:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1b/607e1fc9a8e8838f1a8516f5b87565c9dc415b474ec09479f9443a5a7305/wrapt-2.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c642a83b6703804b571caa3b8b205aacd341b1b37e2b2d89cd70e03e0e9caa6", size = 226604, upload-time = "2026-10-14T00:37:07.899Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fb/6f637ca3e71ea046148dd17622e023df159a16d99762aad4bbdbac11c76e/wrapt-2.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:920f700ef41ee774a1e4778c1f4295e117f1ff3435a7e0cd3e997d10da819d32", size = 239265, upload-time = "2026-10-14T00:37:09.45Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fc/b746f3a72ee7f56d238debeb3842b2569511cf357f5a35f455e24258dc4f/wrapt-2.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3f93ceb0ac4896de45d5a45a8f4e69474da583440589de10b362ddc1db4691ed", size = 223864, upload-time = "2026-10-14T00:37:11.363Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/290a6385393fddcb01bcc83d78c16a24c50a392fe72fb4936999bfe573ec/wrapt-2.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a88370a7d89fcb1c4953a87673fdd7b4a0eb14a1a4dfce49771f0c827ef44893", size = 229426, upload-time = "2026-10-14T00:37:12.868Z" }, - { url = "https://files.pythonhosted.org/packages/29/af/b52bb81d4217ed7f0fe285de6754e44cb5e6a8b316fc78c605743f463709/wrapt-2.5.1-cp311-cp311-win32.whl", hash = "sha256:12bee472452019706fa1d4ead093f52a9683b4fe6617953e15bab9acdfdc013f", size = 100684, upload-time = "2026-10-14T00:37:14.59Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d4/ae9ca837038a7a9aadb906eeca76b42df54e299edb29f8ac2cbad1d55b4f/wrapt-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce3889e3815f97d46414eb574bffdd9bdb41ff70f503097e2707615a87d4e92c", size = 105878, upload-time = "2026-10-14T00:37:16.057Z" }, - { url = "https://files.pythonhosted.org/packages/15/5e/0605567a81c7105446cec682bc2235172a37e2bc817e2de3f5bad95ee969/wrapt-2.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ca7b967e96384abdf7e7182c79f71529997981ece8169f8a8ddb31bc5b57cbec", size = 102825, upload-time = "2026-10-14T00:37:17.492Z" }, - { url = "https://files.pythonhosted.org/packages/66/9b/c7f97d5493a33b5ed01d3c85745f9bdfdd2e5c2785471b8d8b55a3c273d6/wrapt-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6e3eff05ae616671b40d7ad0a504210329e4adc9fb91415663570aca93c5f5cc", size = 106452, upload-time = "2026-10-14T00:37:18.951Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b0/335b0af2930938678fcde954b29780b26308961b93df5e0192fc182e8b7e/wrapt-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c44dd9881626da7d621c23805f26726f6b023cf3e9755f48d092bc9cbef4a8e7", size = 106087, upload-time = "2026-10-14T00:37:20.392Z" }, - { url = "https://files.pythonhosted.org/packages/4a/5a/2a34ba5a468e9d3d6e5b0733280e1ae3c850bfc5f1d681fc0e97f564d1f2/wrapt-2.5.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bfaa998ceeea4d0aa72b40cdd0023d19409504e244b439ff2aa9f01729341c5f", size = 250438, upload-time = "2026-10-14T00:37:21.882Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d5/3d4ad322af74d3ab2a14f69ba844cdd3edefb555edfc1c1976ec0112d5c4/wrapt-2.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6d274ec50a5b208be75596dc44ea253e65deaa6ee3a600babc86dafbb957dfc", size = 250594, upload-time = "2026-10-14T00:37:23.497Z" }, - { url = "https://files.pythonhosted.org/packages/37/1a/3cbf48425ec2c66aa9645218458da1e19e315abb9766604d3c49e579076c/wrapt-2.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1a96e2671c60f9f09ae547b5a815cecb29af16caa68d73693387d0028788cb32", size = 229856, upload-time = "2026-10-14T00:37:25.029Z" }, - { url = "https://files.pythonhosted.org/packages/cc/e7/b2ea57f4c51258659200565af8617d76992b0fe65e6aad7162dd5720ef05/wrapt-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:729d644b6acaf4846a4ef81b037857b66a01dea6d227f827c6d71c0b6d656d6c", size = 247153, upload-time = "2026-10-14T00:37:26.67Z" }, - { url = "https://files.pythonhosted.org/packages/9d/c1/4714743e672ed1084a035a2a4f0edeef7838399753b4856a0dc46ef9487d/wrapt-2.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:859f67bfc31eb7ab55f237b629cd4ab0441b075912446481f910f7d02066811e", size = 226726, upload-time = "2026-10-14T00:37:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/91/e3/c00401bcc3485eb9937c3fe4a1cc8fc3b61800b1378ea3a143ea1c30f6f6/wrapt-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29b62e87fcd6a1893f669abfd02a596a7fc5cfa79fa57e42c4e650a6c170c67b", size = 238843, upload-time = "2026-10-14T00:37:30.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/b5/c16759fb0721e63df92b576c2222ce1f11690a8b300fd91b49c55436865c/wrapt-2.5.1-cp312-cp312-win32.whl", hash = "sha256:f1c911818fb076910ef509f2298dfcb966a54a6ff068eebd459632102cf589fb", size = 100488, upload-time = "2026-10-14T00:37:31.625Z" }, - { url = "https://files.pythonhosted.org/packages/22/d5/39d5a704650f18799f37841442b464edb81cf2015f006eaef26068acc6ea/wrapt-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:c39c7130ea0702c4ab0faf12da1df1e02d5174305c17edf02309e2f058c4114f", size = 106048, upload-time = "2026-10-14T00:37:33.188Z" }, - { url = "https://files.pythonhosted.org/packages/21/bf/65743adeeb5476920c62dad6cded7bc8789e19bd4f9a336d4ac812adb8de/wrapt-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:e089a22ff5af1290b8c759a610830bdb2a829ef9c3d7797e4ee32c2f795ed482", size = 103200, upload-time = "2026-10-14T00:37:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6d/cfe55762435f36107815d56a2cfbebe7e3129b593c47a670c6eb1d7917d3/wrapt-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f98eaf784cd12bc69c77af398084174531007cd81849c962163ccfc6e791f3ea", size = 106068, upload-time = "2026-10-14T00:37:36.087Z" }, - { url = "https://files.pythonhosted.org/packages/01/b9/41642877fe741db56d240833c8822188b663c4c5d52beb087964774035d4/wrapt-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab6db7d2a18d366cc57c2228253cf26443190aba0a6dd0939b3c1e8ac6e29e2c", size = 106190, upload-time = "2026-10-14T00:37:37.768Z" }, - { url = "https://files.pythonhosted.org/packages/37/62/20edad100b93552ec5c172e509a9db898a73d5043ae701fcb6e9986f9d33/wrapt-2.5.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1630201b0e2a96bb26304b7adfbd91a4ef486abb5a4c48377444a0bed749f37", size = 248519, upload-time = "2026-10-14T00:37:39.321Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e9/8d81185bc9a40cfb43d91fc70a1e80ecde752c95dc98f5452cae82037976/wrapt-2.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800c7689154622b0ba2922ceca44a3cf2ef61c3b9a4c4eeb1d8b3050d7ededa", size = 248488, upload-time = "2026-10-14T00:37:40.96Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/8431df4fd81f0dfa83e8ede463eed311d083c5a279a56891dc0396b07b0e/wrapt-2.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b53000b424dc2133eaaf22838a2352d3497f5d7c2e7d9a2acfe675ab7225bb1", size = 227392, upload-time = "2026-10-14T00:37:42.599Z" }, - { url = "https://files.pythonhosted.org/packages/db/8a/ee6f8542eeccad6874faf0b7b2e129952c527a482f1d28940e2111fec2d6/wrapt-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76f230a9b07e3cb66646d265398f579abb6128b1bb4cb97c74b1ae5d09e96f31", size = 244972, upload-time = "2026-10-14T00:37:44.209Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1f/32c59e7fd522409f3863dfecdab5315ee9ba37f96020b6f0adee9d223310/wrapt-2.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd3f878a4aac3c262447ddf43c5f4c18fc67dfc3ba69c4fb1c7a4c4af96abe7e", size = 225316, upload-time = "2026-10-14T00:37:45.948Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d6/1b9abc1244592034c5db744571e17d663f0f1b0ce6c8ba279c60f6f9c3a8/wrapt-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c9480bdee340a1602cae5a777146ab4be3e384fdcb569fffdf8721032314645", size = 237145, upload-time = "2026-10-14T00:37:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ce/8f3b5482f768c1d60fd2557d049c766543fef5ec707037cb410a57eb65ee/wrapt-2.5.1-cp313-cp313-win32.whl", hash = "sha256:dc401274fcc7b15b3b2c12df2ff34024a11925243a7d3daee91c6d7d14f9addf", size = 100462, upload-time = "2026-10-14T00:37:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/7b/dc/6a5735874ea79816f85c1ec9d92139d7073c20d1881c15ff2108c211354b/wrapt-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:09b1893ee4063706574c1813abf479b8b51926633fbdb6f96aab8dc7b0976668", size = 105870, upload-time = "2026-10-14T00:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/08/83/a4e8b5a5a32f8dfc5dad8344f1e2b908f7d8d84b11c3c336bf7f79a5144a/wrapt-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f280c115ea64eff3dcbd68a668ce3f63476a4ba386bbabb318017e286196ea2c", size = 102903, upload-time = "2026-10-14T00:37:52.323Z" }, - { url = "https://files.pythonhosted.org/packages/25/3d/ec1937283863bbe0d90528e09f2b27cfc0dd7e608fc2b65c804967dee369/wrapt-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cf63fffcdcd8c60f223d3967bb92cc4fc2e8b46f09e75b67a6a75e6f47c0fc43", size = 106439, upload-time = "2026-10-14T00:37:53.853Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/cca8afb80dbb9fce6103e59db507a9415291c4dbe97ea055875ff62901fb/wrapt-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f0750cbc2e29e4f3c9529d3587d4e7ed8f60638ceafb80b87a95833b0c5acd9", size = 106378, upload-time = "2026-10-14T00:37:55.386Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a0/e784d7a9fd277a2ee395490ec4df96608b7fe218bb1ef7dced2a1caea490/wrapt-2.5.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3cf273b7e8d2038abb7f0a8c6550aff4f617b9d486a9965c8e8acc96a3a04de9", size = 251505, upload-time = "2026-10-14T00:37:57.022Z" }, - { url = "https://files.pythonhosted.org/packages/81/56/01ebc86b88056f5782b9d50f962fb398c6b82aa11efc98f50c64896d94e3/wrapt-2.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380f72610181883f66b41442cfc7c0f7552b42169efb2113def26e6380013d37", size = 250744, upload-time = "2026-10-14T00:37:58.652Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/0e8eaaf31e2d6e7bf13c6eae2fd5b85eaa24e21e06466e6e7a0f35532689/wrapt-2.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef2a8f006410b6134a0d273ec037fea8cc7a6a914f1bd7555ad9788ad788c6e", size = 231598, upload-time = "2026-10-14T00:38:00.505Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6b/6a3e257e65de6cc0027e78b423942697c74451520ba3797fd86455ec4df8/wrapt-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9bad4dbb4e61624fcce5f301e37f9e743ecae4f1259a3777b3207eb7eba3dccd", size = 247998, upload-time = "2026-10-14T00:38:02.63Z" }, - { url = "https://files.pythonhosted.org/packages/22/38/b2b8f3ee22b05f33f5aefb052844a36a0d7edd1eaff2ff4249f97792bea8/wrapt-2.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9a34640eb6295f33ca23462977de275fe8f3a50ab339b8918b96d69a7451e2e1", size = 228795, upload-time = "2026-10-14T00:38:04.317Z" }, - { url = "https://files.pythonhosted.org/packages/29/39/e6c86552286ac27b855042fb9c229c580ed2d4c3ced5d0a1dad5f5ee8c11/wrapt-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26313f38d18d40a9975123a4ebff9da125ec63ab9ece4f05320a3d8d37d2c1fe", size = 239090, upload-time = "2026-10-14T00:38:06.119Z" }, - { url = "https://files.pythonhosted.org/packages/e7/7a/aea209f64e894573935b17de26ecfa0efe139b63f170a60be9f13734e0f5/wrapt-2.5.1-cp314-cp314-win32.whl", hash = "sha256:0591e6eace0d186c9ef1ecd1244be5a04e98041424cfca425b684ffe4f0d8030", size = 100919, upload-time = "2026-10-14T00:38:07.874Z" }, - { url = "https://files.pythonhosted.org/packages/57/24/847096aa49d42990137ed3b940743c8a6da806d39f6c455317114e8ebfde/wrapt-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:25ed8b1b39234140d5b5c6a273130c7595e0abece417c3ca3cb378fcea5cd0fe", size = 106243, upload-time = "2026-10-14T00:38:09.49Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ff/1cdc742133b9fb8558cdf42b2a6c2699bd7c72f7d0606286ec2f9142e20a/wrapt-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:6201c7e122f40060a9b50696d80deec8f93b1a235ec0443f51d7a8a42f7044a6", size = 103974, upload-time = "2026-10-14T00:38:11.354Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6f/c32dc64900f1970a7f991ff5d06788cd636ca2f3ee2f99709d82577ca198/wrapt-2.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:da847332447db5505162759a4cd5ac374eb8b74841fe97a98ef3de14edd2586d", size = 109552, upload-time = "2026-10-14T00:38:12.965Z" }, - { url = "https://files.pythonhosted.org/packages/e9/73/a9c8cc82b166e3de42f5fbd88089d2ef9b72e87aac7d6cdddb070335c20b/wrapt-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f437dd704abc4ee1bd03bb2d796d362d0e75915e8f3113a7900b3b7ec5f8b47", size = 109853, upload-time = "2026-10-14T00:38:14.565Z" }, - { url = "https://files.pythonhosted.org/packages/f6/48/f341d82e69ae47df2755847af1c744ff0732543482dbc2373e5e57676621/wrapt-2.5.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03aa7d2256309b57ddbf317bff2cae5f47e50ea9ae8d582780ebe0b554347b42", size = 284103, upload-time = "2026-10-14T00:38:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/fd/50/b87c6374377b08ee0783b6c5c31cd41a5e103bc54e7e857e800a0a965550/wrapt-2.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fcccaa1484f7dd1091602970988ab741491f9f974013c844f70e45ac1196b80d", size = 293883, upload-time = "2026-10-14T00:38:17.986Z" }, - { url = "https://files.pythonhosted.org/packages/38/e0/6d0810ae73f7a5180ec366577588ab3ad55fc1bc7e3623e82d5f8528dd2a/wrapt-2.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8078186f719a92693199f1e06c4ec72e1e6d374c2e459da18ed5c39d6966d727", size = 263097, upload-time = "2026-10-14T00:38:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d3/c890a46f4d395a7935e5a7436f374ceefa362eb612fdd39376dd775e0283/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425fcf0e70b27053bd610d57bae975856e7897e3f6ba1456d2b80b9d7fd15d1", size = 289045, upload-time = "2026-10-14T00:38:21.913Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c6/042e30e0d851ca6ea743e6978902527f9166da42d736f074245d1cc54c8f/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b238e955ba34ef2b8897f358b7b868b41b9a02ffd338014b62985fa91898cc4a", size = 260378, upload-time = "2026-10-14T00:38:23.662Z" }, - { url = "https://files.pythonhosted.org/packages/31/a4/5e65f90bf414c2f1c7eefc3d26c33af01d87ba33c4b879db4e1d4ba7fc3b/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25eb4d928a9abeaf70ca786a35861b46d1ab37cc4ce49ea70a070dacdead4dfe", size = 278746, upload-time = "2026-10-14T00:38:25.523Z" }, - { url = "https://files.pythonhosted.org/packages/4b/86/17a85475e218e05225a4f6d65237b12b280596e04520df0e3d8841c4eae3/wrapt-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:df6e3a36170cda0d313be50fe5065948e7f12f3a181b38cbc262e9f2ee4824e1", size = 103415, upload-time = "2026-10-14T00:38:27.409Z" }, - { url = "https://files.pythonhosted.org/packages/27/1c/495b3aebbbe5aebf52ae5f9e8ddd0e072a412b9f5a9bc68e5eba43fa26ba/wrapt-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bc5c0203d383403043fb86c964bd0bab4fcbfb26004ff4bb9c6d02ebc1d608ae", size = 109940, upload-time = "2026-10-14T00:38:29.145Z" }, - { url = "https://files.pythonhosted.org/packages/28/4d/030ecd98da4d052c264290c4fb9f984706c9a19026154c833580e8624a05/wrapt-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a424e8a9776c06aef6313af1d0e3fe6e0838af4241d0c09eb0a3b46f2c9a5ff3", size = 106214, upload-time = "2026-10-14T00:38:30.795Z" }, - { url = "https://files.pythonhosted.org/packages/90/2b/eec5745baaad284fa19232f47796914134e1ea2dd21e289b012aa7981323/wrapt-2.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a18e63910252eb75d8806b4baefbc3a03612502f63eab042e3741b00b719f043", size = 106441, upload-time = "2026-10-14T00:38:32.504Z" }, - { url = "https://files.pythonhosted.org/packages/29/f3/976b0f014a08654289358d41a799c2d24642151b091cfe18a8b91766ee28/wrapt-2.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:183bf0bb893f783c9d22f953cb01fababb9f618e098763f8e66337b575b0647a", size = 106411, upload-time = "2026-10-14T00:38:34.338Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f4/4b94583d9bec0ff0573a5f10fab295675e3b9a64b701c4609b1a1982c390/wrapt-2.5.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1e823aecb3746b8f9e0aee2e1413887871ee2f5c502a3e0ef8d466dbd4adde1", size = 252017, upload-time = "2026-10-14T00:38:36.142Z" }, - { url = "https://files.pythonhosted.org/packages/5e/3c/4f9ba033343b2935a453188f97866f0bd4f7748748ab307aaefb18be3a0a/wrapt-2.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bde5d1b37101b1e9dd3da1f35072e2e7028e9c5e3511f7d76d3fdd4d071b7663", size = 251030, upload-time = "2026-10-14T00:38:38.003Z" }, - { url = "https://files.pythonhosted.org/packages/69/a1/704c761913be404ed893d05702eeda5ff96c5d8448271c28b80101202bcb/wrapt-2.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12d3d2b9d6553df6e2421ab99e1cc5413509076788f57fcb3169f5ce100a19d1", size = 232875, upload-time = "2026-10-14T00:38:40.124Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/779ca20fb8c70238069efd0a2b60ea3da450e57c4f103748db0247f52f3a/wrapt-2.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:521bd5ef2a33171fac08a0a302d51a983c19c3519406c1ee8da7ce29285488da", size = 248302, upload-time = "2026-10-14T00:38:42.138Z" }, - { url = "https://files.pythonhosted.org/packages/03/02/80e13786204ce8e1002edb66d06a3194906c0dbbc038bce7bbda426d0f2b/wrapt-2.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:129cab3c7b21e68e693c2819a95c47f3b1c41a834b931154688c83b6aef6bdab", size = 229839, upload-time = "2026-10-14T00:38:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d1/14c0d041375ae5d0a12445b5c5bc61b109df954cd5bfb852736cd4281cbd/wrapt-2.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8a7c078323e6e1534968cb85488c5eb7ee2b9bbd0f8a291095213a763da40dab", size = 239634, upload-time = "2026-10-14T00:38:45.896Z" }, - { url = "https://files.pythonhosted.org/packages/97/6e/dacc92526fbed1013eb903406ce961da6ff2a1e66b7349a253f439b979aa/wrapt-2.5.1-cp315-cp315-win32.whl", hash = "sha256:736c1de0230c6d24327b14684794214167b2c5ebb6332e28a10f504641b600df", size = 100931, upload-time = "2026-10-14T00:38:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e4/84bbd88554052958ecbbb481a75559b1e40753aba52ec86e3e1efb50ccf0/wrapt-2.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:69fd0fbb3daf7c8c6f5e062847a0061f880f347374d74cf1daba57220fb64cd0", size = 106252, upload-time = "2026-10-14T00:38:49.511Z" }, - { url = "https://files.pythonhosted.org/packages/b7/98/98d4c4524e8af70ccf35b66864be29ea9d232e5a918efc1dbcf5c87a039d/wrapt-2.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:051220e5071fdfb1a6678707c8abb7bbf4824d40f99758394b2b4d64855fb284", size = 103980, upload-time = "2026-10-14T00:38:51.384Z" }, - { url = "https://files.pythonhosted.org/packages/75/d9/4b242519d6d29eabb73cb9e50e645e014eb2c13f022601151953b3e81946/wrapt-2.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:711e73da3d7983547fc9dd208973b6b0c52640822f5d477910ba24622df6ba64", size = 109538, upload-time = "2026-10-14T00:38:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/58/05/e434f56fcceaafb251cc56c03107ae278e99158466b49f4146f7b33a2532/wrapt-2.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:5be9816d9de88f02fce23cf55f392403411d9bd9c7ae57fdc965a43b22e2de5e", size = 109874, upload-time = "2026-10-14T00:38:54.941Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/d2c0b34d02804018225279a157307b873c8d0f8452790efdd7f0004387a8/wrapt-2.5.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b3f410c416752e1dba53d361e2e6562f22c2c3ec855740dfa5836e061b22571", size = 284866, upload-time = "2026-10-14T00:38:57.081Z" }, - { url = "https://files.pythonhosted.org/packages/71/6f/2b56319c0565d9a6b63eeba2f11f3e699dc324aaa460f1aae079bd4507b0/wrapt-2.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:094b847491b813b6e6c1775e03770930d75078c0821adf929ac712830951ef25", size = 295154, upload-time = "2026-10-14T00:38:59.007Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1b/9ac4238a1a839457d6b687b9e9c35d57ba1b2050a42d6bf8c93176bc1bdf/wrapt-2.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26d8ea2ec6818aeb656bd8a9e745a6f1fb0edfcd8f54291ccd94f62eb5f5e3bd", size = 264914, upload-time = "2026-10-14T00:39:01.154Z" }, - { url = "https://files.pythonhosted.org/packages/4f/95/9faed8e5f6e5431edd36b2cfb4f305df520c197ba3639e1c78d11c70a068/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a526227efe17dd94bd16b123d170f879bce42c15f10eb92495a745f54caa943", size = 290238, upload-time = "2026-10-14T00:39:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/b5/52/cae26590ef8ee46b55aa8b211c507f6e5ec0fbd241a6730bf7da024b5dab/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:36d7d0ad593c4f1a651e4032de834db59aee1a929ee396cd483895b673328e51", size = 262208, upload-time = "2026-10-14T00:39:05.008Z" }, - { url = "https://files.pythonhosted.org/packages/00/f3/34e5008307be4169592e99de52946cd8800b98097e2a145da11484c7b3e4/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:89d9a8607b7028054bb6fd01d437f205534a5d59d53c3665d15949a99a2fce0d", size = 280005, upload-time = "2026-10-14T00:39:07.048Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f9/64e000aa84a88c52a481c7c8b011c80a8ae60bd5f68598e567a918a624a0/wrapt-2.5.1-cp315-cp315t-win32.whl", hash = "sha256:ad81bf81b0a0b6c6ec74169638202851962843e86749570c463eecc55072f93b", size = 103378, upload-time = "2026-10-14T00:39:09.336Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e4/69efa7c6e8535c5188e041ac278079949fb2daaa97e6f08beb91cf31b3d1/wrapt-2.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d5b665a43fe0d3b390cbdd3c003d61c92fa07bd5e3fb1ed3f47920c2d03cd9fd", size = 109928, upload-time = "2026-10-14T00:39:11.156Z" }, - { url = "https://files.pythonhosted.org/packages/13/77/6e414b3388b9f1ecb76107ef4a2aae501f1bdfcab85c8e34ef78f7db22db/wrapt-2.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:6405ff2160af9d59132ebb076eda0304db44d9d09809582932412ef7c0788a36", size = 106226, upload-time = "2026-10-14T00:39:13.061Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0c/7da7513ddcc8f1d831ec4bfbedc9f7f174ecb91042bc16916fc1e0d06b22/wrapt-2.5.1-py3-none-any.whl", hash = "sha256:c6e6c226b1ca5402d7ae5fb34a0d21f1b49124fe4200e5884d1e19e53c47ac1d", size = 81849, upload-time = "2026-10-14T00:39:37.441Z" }, +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/b0/f752d19e1e83ab8bd64aeccfde32ff086aee3ef08520f7340b69a8d0a4d2/wrapt-2.5.2.tar.gz", hash = "sha256:551cb49e88b3722faad780303833bc4fa70685fb1773270fb98b40d39d9fddbf", size = 186066, upload-time = "2026-10-29T23:06:39.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/c2/b4bbaabf73ea9ccc2e322a11455a95d7d9dcae5bbcc6fd836c5f676a082f/wrapt-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b240a879405591da10e2d20c2d1ad29b978d40ec37e24801296cfee1d4c04f9", size = 105761, upload-time = "2026-10-29T23:04:28.866Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d4/57db9384ffb339f29b6712cc1d7d4ace29d07670756becf9ad336728ca42/wrapt-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b632d90ac0d04e3ba8821b19954d6c36ea8f714b75db53c58015025fefccb1ac", size = 105933, upload-time = "2026-10-29T23:04:30.45Z" }, + { url = "https://files.pythonhosted.org/packages/98/00/a0effd5872aed3271b28b4740a71bc2d7406d33b4e03eac01ae099ac8569/wrapt-2.5.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0743950d3dda9940e56f81f5334ba051c4fe2e1421e4e4a4ef8eb8fb86a90894", size = 240286, upload-time = "2026-10-29T23:04:31.855Z" }, + { url = "https://files.pythonhosted.org/packages/69/22/8e9ef3a15115f3cc01f2460aaf9aeb04819704fefd218086642c7182be95/wrapt-2.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594c6d10b9cfb951a09f7338531a41d31194e7eee9dee206c0a8992421682719", size = 241793, upload-time = "2026-10-29T23:04:33.501Z" }, + { url = "https://files.pythonhosted.org/packages/15/7f/b2912f3d045fd9a5f30d9d58f3b4df8921c87e902524a32d7eb0e0e315cb/wrapt-2.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4362b3b4e4b050b8f1f518e0c71a09f2e938a9f62ee559ea1743a13b3c9e8f1", size = 226781, upload-time = "2026-10-29T23:04:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/cdd488014d5010b052fdd9c368d8598accd9eee766d98e698cc8510b364d/wrapt-2.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:81e685c7662fab9958a852b9c86f3b5b8eee45d047a2766d6c23b63ad1eee423", size = 239441, upload-time = "2026-10-29T23:04:36.296Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/6605b2fd612ea1635d0a7cebbdc05cdb59c97a07ee9c418a5f43a49cee53/wrapt-2.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c6b4bd8c662c333518a9aedb35bb9f4f7d1584fbef4bc1507a6c6a47357c628e", size = 224041, upload-time = "2026-10-29T23:04:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/42/9b/6a8acfd407991b50b9bdbaaae5551cc12ba487a8a0973c87793dac621f1f/wrapt-2.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a7efde469ad904b8e05f98c76d63b437f4756c30ec7b9136bef02bdb5d1b14c", size = 229602, upload-time = "2026-10-29T23:04:39.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/6c/9a975e802393c74af9879ca938b298a00fd59074ee90abf4ec4bedcdd109/wrapt-2.5.2-cp311-cp311-win32.whl", hash = "sha256:73d432588d1e0a8fcb88d46ef2733e848ff0c3e14111a0420c77199fd2a8cd30", size = 100856, upload-time = "2026-10-29T23:04:40.884Z" }, + { url = "https://files.pythonhosted.org/packages/46/aa/196ab4e3584e547fcb6c4a622934aae14077c367b86f57a5ab5e76a47203/wrapt-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:04703a2a4eea122c3a6b3af89e72bbbaa20779e5c7199c3d06e3fcb6601bc7d4", size = 106052, upload-time = "2026-10-29T23:04:42.232Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5d/7a699446d344bc74bcd607a0cf09087285a253eefc6449048c0df7b5a3e5/wrapt-2.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:53cf804402d49fa2c920a90dfab971a82bca67fb43e54adc7c3b24c054485b4d", size = 103001, upload-time = "2026-10-29T23:04:43.529Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fe/9ccae0a147d0732397f54ff232b1a0da148a4099f17c1b156b9d3e79127e/wrapt-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bec3b6b1ae2f7ed763fb0ba1776b9b5f11283e6ff40c9cbe3a855ea652c326cf", size = 106628, upload-time = "2026-10-29T23:04:44.962Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ca/a61d994df49fa422abd859e525e04b95ef2888e3818a0f451aa7d4d3527f/wrapt-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e57d79e3338b3cecee1dd80d6ef8f945f8ea7b92f0d8d5591f840ffa0afc14dc", size = 106264, upload-time = "2026-10-29T23:04:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/dd/26/a6caa9a84ef5db4a273787949cea7ab51f6001d2548768af4c8e3c49debd/wrapt-2.5.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:092472b7b1494deef40dac5e9f6295013a9d51b112b5fbc4350f64dc2574e46a", size = 250614, upload-time = "2026-10-29T23:04:47.735Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/2189571e711ab31cb7f682c739d05f033ae0e83e12eb4d5d21473ca2cd1a/wrapt-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d108bd98ab2c5ccbf55cf2154e0a73341df930030df2e01ee35d71a44a99d3ad", size = 250770, upload-time = "2026-10-29T23:04:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/80/8e/46a6cc2e8215df7fae292178a355df729547ff9b912686823201d38be269/wrapt-2.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:858511731d6ede4217960c5fecdb5ecbf90e306dfd93b837395658d61374a498", size = 230031, upload-time = "2026-10-29T23:04:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/97/38/dff531d0125fe880e05fe5fcb89f144d25ffbe95aa9a2c50539eabde7567/wrapt-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b6885250b809532a634acd56e360d57c201c1cdc75cbf2b5e106241ece6cbc3", size = 247325, upload-time = "2026-10-29T23:04:52.16Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5b/f4a78df4c52175d53ff095f2f5a54206a2d16e542b1dd2cdcaed901fe432/wrapt-2.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c60a5cd8b7b510056f2643d94f1c144533a568ade2891a9dd35733f5e0055777", size = 226900, upload-time = "2026-10-29T23:04:53.559Z" }, + { url = "https://files.pythonhosted.org/packages/20/6f/8eab144c717300f76f1337ba58091f4de5da0f4d772727c9f41135b325c1/wrapt-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:40e03506f09dbf4854fd69848e843dadd9b7fbbdd217837cb27b37fb8f0b5bc3", size = 239017, upload-time = "2026-10-29T23:04:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/59/1a/98e278ef2f8952a34f847a91f75899f6385762b07577c51a5ef82c768dfb/wrapt-2.5.2-cp312-cp312-win32.whl", hash = "sha256:ac00ba7d22d55d3c8d085005ce4942ff3bb118d480f889b5984f1d3454052917", size = 100660, upload-time = "2026-10-29T23:04:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0a/ab645f8b4c594cac2db49bbc805bff97a9d3f2944061b865f67e0351b6c8/wrapt-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:36021ef761856bdbc10d5f3da9a95af93ef60d6acf9c72c8f849937ba2b902aa", size = 106220, upload-time = "2026-10-29T23:04:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/37/1e/232868b68b925ab29ca5acbec4ad54ee42a853fcd8f596bf7e0e0e3850f1/wrapt-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:81bdf0bd141d9b1706a0751b2ee068c7afe6be4042712d427b32b3072d5c40b9", size = 103377, upload-time = "2026-10-29T23:04:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7a/38f08a89ca8faa9b788a857f109b5e4cd1f25671a5157e2218072f64aa76/wrapt-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:88f982c2bb306732271aa15cded4002ce28b5d9023de9d11e927da4dcf65e441", size = 106242, upload-time = "2026-10-29T23:05:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8b/2fb9b2522b7e1e7534a72fec0040bbf3d273129ec0a98316e7d37b09aa35/wrapt-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:269321b17467a10997cc6e14793bf1816e6ce488f9711c74b72552466d206739", size = 106366, upload-time = "2026-10-29T23:05:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1e/770d73e0cc82a41602cb2cb184769c28bdbaa9b998944d577a1097c18da3/wrapt-2.5.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b039b3a296762db1908d6108365225393c4fb33c1dbc5f00aa36f8302eaeaa0", size = 248696, upload-time = "2026-10-29T23:05:03.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2b/b4362e803d870451b5e701df5e30ded2d6fb1d9e0c01eec4dc7fa8332b57/wrapt-2.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1af6136ac5bcfab17190a70b25d8437eb8d8f9ca2b821a0655d39275a8cb7e7", size = 248663, upload-time = "2026-10-29T23:05:05.084Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/244dbd2d86285857898be5f7632e8375161953e645f250ac9778c058ab9b/wrapt-2.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d4e1cdf2efdc7181c8177f6090886e22278e70f7257df12863d2f52a9dcbbd7", size = 227568, upload-time = "2026-10-29T23:05:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/62/0e/15ae5812d510fbfb65acb3a63aecbd8ffb4b944b2fd0131d968e31df89d6/wrapt-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e33578755e07edf3a70df3241f4cf376dad507e2cc73e737311bb00b5067f4d7", size = 245147, upload-time = "2026-10-29T23:05:08.322Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c2/e611b881c05f7f2a5c88f9d89236afb091b8adba3ac89ff9040b6a837be4/wrapt-2.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2bf23cde96e0f771a6a2c68973c1ded45c9707f8bd519ede30ecf075f3c720f6", size = 225491, upload-time = "2026-10-29T23:05:10.102Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/bdb253208ec907097809e34c25d199cd66686a7974257e08b57d96e3176c/wrapt-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eff91af619ad5d587f8855f6c02164e12d33d205c702eb73d487deb499520265", size = 237319, upload-time = "2026-10-29T23:05:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/67/86/74205217834201f27bc73a76b4db67f488fd07787e12a045fb2b39f09c29/wrapt-2.5.2-cp313-cp313-win32.whl", hash = "sha256:11655df8e17658a67628cdc97fe95d6b263e023213cf18067f21e5ec0b0285f3", size = 100634, upload-time = "2026-10-29T23:05:13.073Z" }, + { url = "https://files.pythonhosted.org/packages/e0/56/7c732aed39116853d74485b4431a0ea37b05bd2bf250bdddc55ceb64f8c0/wrapt-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:5726faa7342ba473041bc1a276486d232b42e3927d35427729b266b1b5ecaa42", size = 106044, upload-time = "2026-10-29T23:05:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a7/c4f7ae891bbfef566aa63effe846f6804dac4c0461601993f4c59deaf955/wrapt-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:e1fb4af8aac3573770ed3e64efc39d953342b12bcb6e6479512a945eb5557523", size = 103077, upload-time = "2026-10-29T23:05:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/a259acf2b59eefe400b2a1c6b45bc19b0d0b583722426c9dabb276cc0d19/wrapt-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:229859aebfc909626ea6b5a3ae9523aba43c63fd828f876f2a813f4a5767726f", size = 106620, upload-time = "2026-10-29T23:05:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c5/b34ca079b7906eb043c53c06896b4e42f3ca9efa97ac10e4e254c88e2ca6/wrapt-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c1ea16d7dfe7d235d29435307b1539495b4925e88b135d532892973a04569245", size = 106555, upload-time = "2026-10-29T23:05:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/e2/46/f66042c9ed3626cdcdf641105f091964f6d38cd406cec96479c4a3f70640/wrapt-2.5.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59b7e6d1dea7c3f38bab21954f587499c27c16ea537058eee4add6e4da1780ea", size = 251681, upload-time = "2026-10-29T23:05:20.035Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/1ebe33ff8053a94b3e1a0bafe31ea94b5e9296f9c5098cbcafc4ee2bcdc9/wrapt-2.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfd508866c421687a99bed9ce5b7b4f26fd9b93d3753685d81947ab37d54c496", size = 250919, upload-time = "2026-10-29T23:05:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bf/ee90a8674c8f2179ad7aeaf8c5b875fa33a12fec7bf1ccf6cbfcc31fbbf0/wrapt-2.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:459bb66cd4bc43e35cba1f1ab2bdfb9b507e0d8dc0ec40c93a060c9cb0c5415e", size = 231773, upload-time = "2026-10-29T23:05:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/610ef6bf4f35e4719a5edea6bd0c4d948921d028950bc9669d1fa9234af9/wrapt-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a8626135ecbb0b892ca79c388581ab0ef57e087f09dc0e51a2f34185d3682055", size = 248171, upload-time = "2026-10-29T23:05:24.481Z" }, + { url = "https://files.pythonhosted.org/packages/c6/10/6f12cf42bd9a34829c902c8a617c8151a4e16e7d7cb015289eede5813f39/wrapt-2.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ffa0be31f651da63ee51e5b74701339feac1860b561a985c7b4869d4600e800", size = 228970, upload-time = "2026-10-29T23:05:25.904Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/79417514032b9015b772bf5bf006ca542937b5f6c483b9ab092f9ceff8e9/wrapt-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ab31d929ca8b2b7bf9320ad2a927edfb1841609951a869a9b98723b285ffbc6", size = 239267, upload-time = "2026-10-29T23:05:27.502Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ce/092f2723758586973c94afa8c38a206ce3720ec5cd3edb64ee9c8b27a1c4/wrapt-2.5.2-cp314-cp314-win32.whl", hash = "sha256:afd99530994449aaac9a58e1e268eb080bb1ffd758be43ab2f8a249ce8970c9c", size = 101091, upload-time = "2026-10-29T23:05:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/86/201fb9180290373b7fdff18e8d600151694fdb0b10892cdee77c13eb7e6b/wrapt-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:fabc60720d1e4a36bbe8aaf1c32024f04c6674991313f5acaca5eeadb564c600", size = 106417, upload-time = "2026-10-29T23:05:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/73/a8/cc313dc9230f4b5029e24867a1fc1dbcb929b792b02ac5fb2e0d79df7401/wrapt-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c17ca5ce83c7145eddaa20073398ec2192dad65c8d8ff4d9b78fd2917c46a75", size = 104145, upload-time = "2026-10-29T23:05:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/62/04/15e0c465088fbfdab321a672c88739f68429a18aeb81ad7995a0a5cf2065/wrapt-2.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60863d874cc6ac917bbf44324e3f195564064dee15c84e83b19e1fbbccd3169", size = 109731, upload-time = "2026-10-29T23:05:33.131Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/91d6f77257afabafe19bca698b41d60dde7cf49b59b0126eb25ed6a3a90e/wrapt-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d41c45517912610de55546188695e2bd0fbcd70b585087cd759f85a96f47be7e", size = 110032, upload-time = "2026-10-29T23:05:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/9c/84/40901e8ba08f6f18f213fea51daa41a8107ccaae4e0b6e13b842f02f5feb/wrapt-2.5.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:48fd5026977adb3e2fda292a6a796967b511935d23f97dc2ae1f127a3a8639d5", size = 284278, upload-time = "2026-10-29T23:05:36.033Z" }, + { url = "https://files.pythonhosted.org/packages/89/af/6bfdffc2688a78019762f0b14171ca47c73fae1421c3324c0850135b212c/wrapt-2.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d7639c3287d4efcdc2c8837f1854d1ecf27aae761714ba3d95e61fe8eb48db3", size = 294060, upload-time = "2026-10-29T23:05:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/9a4e3aed00d6d877d2646072d9b6b4c9a558a8ad79a18d3abafc1079cd07/wrapt-2.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeaa40251c40e18c5c97ef11d5dbbad28a7b7b99fdd7a7469662799100d74dc7", size = 263272, upload-time = "2026-10-29T23:05:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/0086e02aae9144a0fa029fb0351a8b2f91fbd28eb191c8f2ced3831cb1ba/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b9ce67ec8803e6c56e78c9ebf726f61116516212c28dfa3683d0962e15cf9a62", size = 289219, upload-time = "2026-10-29T23:05:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/e342f997166b7d433abc12720b6e30793fcf3a36c5457d22261558b61411/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4d5faf5c3e435207b3a51294ed5a00e5e17e3842442ccbbd864ee9bc0efc63fb", size = 260554, upload-time = "2026-10-29T23:05:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/65/64/48ddff54611f6e08ba70adf1b3dbf36c7b08b7aa5dbf7c65a03672b0eed1/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2861d50a4f330fa140ddc2a9ddee33f7e4f9bdb649a23dc9be8ed6310ffa47fc", size = 278921, upload-time = "2026-10-29T23:05:43.489Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bf/0ca422495491f22656b401832184a87b1e67d1dfb7f31ac8d60525c572ce/wrapt-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:46cee4400437c9f33e755522be9e7ce6ebd097273f464eb6f57bee09787f0c59", size = 103586, upload-time = "2026-10-29T23:05:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/69/17/922d99009a228907cd0a5f5d590bbc176dd70527d3eb06f8b4d74889edf8/wrapt-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1ad706a42b139ad7f03f32aed57da5e10fe8ed707e29849cfa94f1d49cba1405", size = 110111, upload-time = "2026-10-29T23:05:46.458Z" }, + { url = "https://files.pythonhosted.org/packages/68/45/d168c09643e3207c5618b33a5636137f298b9ded38b3a33fad67dc343403/wrapt-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:450cc93f5a2d58d4a74d384d52db4b41d5728065c0057c9bb4666c6115f2a2c9", size = 106384, upload-time = "2026-10-29T23:05:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/3b2b95cd60b78f9450d748854e421fcf48bc350d76ca9e0fe048b87ad796/wrapt-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c17ec4310cc06f864799a4c37c1c716d5dcf05ceead2e63ca992a9466903b73", size = 106621, upload-time = "2026-10-29T23:05:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/16/12/c430cd1289711cdf2b6b1c469d5efa527f4815fabab32bff3249545344da/wrapt-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:430070776fb30d925a7a1e13d68086e4731b63eedf4c43ee62fc4c8108920cce", size = 106590, upload-time = "2026-10-29T23:05:50.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/4b/d7230c57ca30023a5630bf5cc0e092ad1cfce4d999a309b8377a36b3c249/wrapt-2.5.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99978d196df45c115273c6119e4d0dacbc67bc267a196506625bf7084b741f0b", size = 252192, upload-time = "2026-10-29T23:05:52.344Z" }, + { url = "https://files.pythonhosted.org/packages/fa/74/6dc841c96b9906e0c466aaeed7f51162129cc8745a91bb28168aa3b45194/wrapt-2.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:217798fb0cfb31479a264f841b0242bc1bed1ffc70d233e8444a876cec57928c", size = 251207, upload-time = "2026-10-29T23:05:54.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/bb/25e710cc9d3bd25d81d46791dd25029d6f9b9d44b990e3d81755b549ff71/wrapt-2.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62b339565cfdfcc0ba71c527b4ad13ac9de25504d474fa4ae5db95d1c7e4c6a2", size = 233048, upload-time = "2026-10-29T23:05:55.796Z" }, + { url = "https://files.pythonhosted.org/packages/4e/80/672640d799862a36abae740e778146dbe155ec8ffd6211b3a99240f599ca/wrapt-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:84619ca6b6f67a7dd53754ad402cafceb4ddc9d8981f144f62718b783f05786e", size = 248479, upload-time = "2026-10-29T23:05:57.346Z" }, + { url = "https://files.pythonhosted.org/packages/f6/34/91eb6724eb7cf64b77637d720ef71cad1ec6e4ce2d15eb548670c47078fa/wrapt-2.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:08d1d7bcb7dd277b7b976fd914455c3c1088f5c101ac7469ae5a5b8c03be0a0a", size = 230013, upload-time = "2026-10-29T23:05:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/f1fc0c3394819c9a6a3c0d0055b06614a3bef7ad133cf18f9c09a2b2141f/wrapt-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4e86f3543c67d72b4d4c08c3e351523c2d69cd05932df65937d79bc472e9d62f", size = 239808, upload-time = "2026-10-29T23:06:00.266Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/70f9e71674d7b6c21c8e8cafc3016f773dc3acb31a131f23ab4af8c1effb/wrapt-2.5.2-cp315-cp315-win32.whl", hash = "sha256:035cd71e2aadfc0ad366ab4785b49e62093370885ea4b527b61b308b7a92ff14", size = 101099, upload-time = "2026-10-29T23:06:01.777Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/fdea9e35605a6e7fc71556068ea184fb3e9cc09d1e3be8ca8f26d16339c9/wrapt-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:b9f41029f9bc6ba80755b9c07f5e77e9e7aafb71c625b9f67614e4ebfb24302a", size = 106423, upload-time = "2026-10-29T23:06:03.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/50/8c41d8ba868cde9afd5b68e18f19b94300d091c409ef081b80c2aecda653/wrapt-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:ae84013e89ec1874415549a46cfea373e28ddecbb72e1f5456cddfa2b76a28e6", size = 104152, upload-time = "2026-10-29T23:06:04.356Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/b43d341b036e2862550eb7fffe2b4130015c39c861f30eb34014a2f032d2/wrapt-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:b680bef5200899524667f813fa34dcfde9313fef9f9fa432985d4bc7b4195ba3", size = 109711, upload-time = "2026-10-29T23:06:05.768Z" }, + { url = "https://files.pythonhosted.org/packages/be/52/8012aaf046264e27969f79bc94cd2afb4729338f5434b90edd3d4bb60b8f/wrapt-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:da51f5ac290f2e024ee842799c6592f68cd880c20f6b3a54305d1b6aeab459a4", size = 110047, upload-time = "2026-10-29T23:06:07.309Z" }, + { url = "https://files.pythonhosted.org/packages/54/a7/628c315d19c6027e8b0e700c0dd7d5c3fd662329c7aa1294315064b87cdd/wrapt-2.5.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ebc07822649eda442382c1d20983bc6ec5aa2bf79a600da11b29ecb62a0fb2", size = 285041, upload-time = "2026-10-29T23:06:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fd/4b418c9655bacb5ce8b0aed894f8355e12940bde74237ec45d9396e1f87f/wrapt-2.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb6e959c1e2c32bbad291f83af6320c8c46bb64e256801e85f55d88a09522d5", size = 295329, upload-time = "2026-10-29T23:06:10.489Z" }, + { url = "https://files.pythonhosted.org/packages/92/24/dbe9776fded6dd5f2bfa1446ec04a33afe5afac1d5fb89cd7f5e96fbc61d/wrapt-2.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0541e62f7b9c0d42130e3469d6ac3b1767f019314ae795646a2850a75c64412", size = 265088, upload-time = "2026-10-29T23:06:11.963Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/4103576d31bed902224fed8b1d06b94313491504d1d644e42bceba4de880/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:18819c05409a161999be68da8e451636470c35f77e25c8201825a33a7d88c89c", size = 290413, upload-time = "2026-10-29T23:06:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7c/944e95c25232a5805ea9f25cf2ce90386262db025ba04fbb61f451d9523f/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2594e0be4ee09b2f19828265a39df90e591def44215fe8c5b8d6b83aa4cc18b5", size = 262383, upload-time = "2026-10-29T23:06:14.887Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/a4b66497e3bb63d2e94ea1605f4beaf5b6b5dafbb6ce41abd727a6fcb7b5/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:32c0387872e67b9dfdd2b599bf31fe127e597ed8526c5f192cdaca57a491e9dc", size = 280180, upload-time = "2026-10-29T23:06:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/91/bd/28d046dccfcade6ad62ad19f2f805e5b2e3f3463431307c3b8bd54e24e7f/wrapt-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:9cb33f293e3aa04b36f7d165dc05b42db85e0a88e20a8fd81f60426d1f17d535", size = 103551, upload-time = "2026-10-29T23:06:17.732Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d9/aef3035a3e3d918eb51e7db454af9e06919f0c5208fa0a9315548fef7734/wrapt-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:7f89423965b23aa62a90bf21c20353056a829ca9a60c01fde9aec9876c99392e", size = 110099, upload-time = "2026-10-29T23:06:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/56eddba6249fae5361b53eb62e2e25ad86cabec4632d943d356aba8dea1b/wrapt-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:fa642d3781c061579cb138773add4a55b16f99de9ebc41603e89a756cf9e62c6", size = 106397, upload-time = "2026-10-29T23:06:20.826Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/1ea57b936b70f98e8e7620a352578096d778dbee95b262f73c066d210ffb/wrapt-2.5.2-py3-none-any.whl", hash = "sha256:61eb54e9540ea522c2c4752a57549a83d3c67e6704ed708403d28e067aaf90f0", size = 82025, upload-time = "2026-10-29T23:06:38.56Z" }, ] [[package]] @@ -7357,6 +7002,43 @@ wheels = [ { 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" }, ] +[[package]] +name = "zensical" +version = "0.0.74" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pathspec" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, + { name = "zensical-ui" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/41/147eed1f072917de5d91e2aa0c2204edf87e5e9c7cd34c314177969d5281/zensical-0.0.74.tar.gz", hash = "sha256:f2ddd17daead93fdbcaa42b5bdc367f9a4808bad282fe706763cb52dba29d96b", size = 514731, upload-time = "2026-10-30T22:09:31.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/a3/4031992e04a323f09b3fb89531fdce6c13f83650533a39809ddab9b4a4ef/zensical-0.0.74-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:44f5afe36d1fda5a80a2e9b24cecd25e2d6e6f1f05845299284108d7be40a48b", size = 8621183, upload-time = "2026-10-30T22:09:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/6d56d6886637927204180ddf8480483f9ba4f838cf23f871c02845e8b1ef/zensical-0.0.74-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:912a6a5eb834b283e31ca89341026ccb0bba260351f9123139dbb476a3daf505", size = 8217741, upload-time = "2026-10-30T22:09:12.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/deedbf55d985629af31cdbd126a83d60c89440d32d94fd86c9ab7b13a215/zensical-0.0.74-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7722e1c2505925a477719b79fdc25defe53ce032bab1d769bd4861b130cf149c", size = 8661389, upload-time = "2026-10-30T22:09:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/48/77a87b588801a6945a274415bffd0ffe7d7781424bec449d54422780784f/zensical-0.0.74-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9da81ad4bb42bdd92a52d9e42623eaa25c936fa0df0d26e507dcd282e8bf85b2", size = 9151632, upload-time = "2026-10-30T22:09:17.611Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/c8ffff0de58e6f8810b1d7609442efa2919058c8d1035d266db4b2aad7d9/zensical-0.0.74-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4df8506f0c13226436c6955203b3332e5334b579a1daeeb3c9e7749c726e5327", size = 8830888, upload-time = "2026-10-30T22:09:20.736Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c6/3eff7d34d94fe1978637d60094db4c407e9f3779a41f37fa678a6d8e7356/zensical-0.0.74-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a6fad6241a6fb04012ca87bf31e11f7cc3b3248a84fe4bba3e3b86a3506cb09", size = 9422666, upload-time = "2026-10-30T22:09:23.882Z" }, + { url = "https://files.pythonhosted.org/packages/cf/91/dc6a6783b62d82ad872beebb14339d0e9b8372dd8b74bd5c4b388d8dab1d/zensical-0.0.74-cp311-abi3-win_amd64.whl", hash = "sha256:491ee04190f8fc0b731a40d99062f979a508b522265c5042cbd79eb7a59e250d", size = 9356286, upload-time = "2026-10-30T22:09:26.574Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/fa31d226655699c974137499a98dd5a188882f0c9460ba584107e072ad2d/zensical-0.0.74-cp311-abi3-win_arm64.whl", hash = "sha256:a43b3801fb6e90f774d32b4e485a81cdf028a5765d0d5398d28cf406be888731", size = 8750302, upload-time = "2026-10-30T22:09:29.718Z" }, +] + +[[package]] +name = "zensical-ui" +version = "0.0.41" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/a7/77f1d045900f37f703bef9f64af8c8ca3e9419ca6dbfbc7885e33ca8f2a1/zensical_ui-0.0.41.tar.gz", hash = "sha256:2268e3533a2e186eb7b67dcc8ccdbfc5994fe114cda9382a505f306256df3d88", size = 4063232, upload-time = "2026-10-30T19:06:00.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/84/bb9f8022f2a4fc57c53e135d24318c880205038da644432d443598e752d1/zensical_ui-0.0.41-py3-none-any.whl", hash = "sha256:f0cd5b4351c130257df65b2100fc7116e72c777b1fbc596fee64a9f5de4ccdad", size = 9961148, upload-time = "2026-10-30T19:05:58.612Z" }, +] + [[package]] name = "zizmor" version = "1.31.1" From 7945fc6798fafdcb319be518a0b8f34dca301496 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:16:59 +0000 Subject: [PATCH 23/35] test(mysql): allow driver-native integrity errors in adk owner_id constraint check - update test_mysql_adk_owner_id_fk_constraint_isolation_and_cascade to catch driver IntegrityError exceptions - fix frozenset set difference typing in postgres and cockroach data dictionaries --- sqlspec/adapters/asyncpg/data_dictionary.py | 2 +- sqlspec/adapters/cockroach_asyncpg/data_dictionary.py | 2 +- sqlspec/adapters/cockroach_psycopg/data_dictionary.py | 2 +- sqlspec/adapters/psqlpy/data_dictionary.py | 2 +- sqlspec/adapters/psycopg/data_dictionary.py | 2 +- tests/integration/adapters/mysql/test_adk_store.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sqlspec/adapters/asyncpg/data_dictionary.py b/sqlspec/adapters/asyncpg/data_dictionary.py index d63393b7d..a2978b941 100644 --- a/sqlspec/adapters/asyncpg/data_dictionary.py +++ b/sqlspec/adapters/asyncpg/data_dictionary.py @@ -57,7 +57,7 @@ "ddl", "system", ) -_POSTGRES_SUPPORTED_DOMAINS = frozenset(_POSTGRES_METADATA_DOMAINS) - {"system"} +_POSTGRES_SUPPORTED_DOMAINS = frozenset(d for d in _POSTGRES_METADATA_DOMAINS if d != "system") _POSTGRES_SYSTEM_METADATA_QUERIES = { "settings": "settings", "statement_history": "pg_stat_statements", diff --git a/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py b/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py index 4658fcf6c..157546b46 100644 --- a/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +++ b/sqlspec/adapters/cockroach_asyncpg/data_dictionary.py @@ -54,7 +54,7 @@ "crdb_internal", "system", ) -_COCKROACH_SUPPORTED_DOMAINS = frozenset(_COCKROACH_METADATA_DOMAINS) - {"crdb_internal", "system"} +_COCKROACH_SUPPORTED_DOMAINS = frozenset(d for d in _COCKROACH_METADATA_DOMAINS if d not in {"crdb_internal", "system"}) @mypyc_attr(allow_interpreted_subclasses=True, native_class=False) diff --git a/sqlspec/adapters/cockroach_psycopg/data_dictionary.py b/sqlspec/adapters/cockroach_psycopg/data_dictionary.py index db520016e..e4b5e39a7 100644 --- a/sqlspec/adapters/cockroach_psycopg/data_dictionary.py +++ b/sqlspec/adapters/cockroach_psycopg/data_dictionary.py @@ -54,7 +54,7 @@ "crdb_internal", "system", ) -_COCKROACH_SUPPORTED_DOMAINS = frozenset(_COCKROACH_METADATA_DOMAINS) - {"crdb_internal", "system"} +_COCKROACH_SUPPORTED_DOMAINS = frozenset(d for d in _COCKROACH_METADATA_DOMAINS if d not in {"crdb_internal", "system"}) @mypyc_attr(allow_interpreted_subclasses=True, native_class=False) diff --git a/sqlspec/adapters/psqlpy/data_dictionary.py b/sqlspec/adapters/psqlpy/data_dictionary.py index 279274a1d..a45f39843 100644 --- a/sqlspec/adapters/psqlpy/data_dictionary.py +++ b/sqlspec/adapters/psqlpy/data_dictionary.py @@ -59,7 +59,7 @@ "ddl", "system", ) -_POSTGRES_SUPPORTED_DOMAINS = frozenset(_POSTGRES_METADATA_DOMAINS) - {"system"} +_POSTGRES_SUPPORTED_DOMAINS = frozenset(d for d in _POSTGRES_METADATA_DOMAINS if d != "system") _POSTGRES_SYSTEM_METADATA_QUERIES = { "settings": "settings", "statement_history": "pg_stat_statements", diff --git a/sqlspec/adapters/psycopg/data_dictionary.py b/sqlspec/adapters/psycopg/data_dictionary.py index 176f35e94..e0c245ea0 100644 --- a/sqlspec/adapters/psycopg/data_dictionary.py +++ b/sqlspec/adapters/psycopg/data_dictionary.py @@ -59,7 +59,7 @@ "ddl", "system", ) -_POSTGRES_SUPPORTED_DOMAINS = frozenset(_POSTGRES_METADATA_DOMAINS) - {"system"} +_POSTGRES_SUPPORTED_DOMAINS = frozenset(d for d in _POSTGRES_METADATA_DOMAINS if d != "system") _POSTGRES_SYSTEM_METADATA_QUERIES = { "settings": "settings", "statement_history": "pg_stat_statements", diff --git a/tests/integration/adapters/mysql/test_adk_store.py b/tests/integration/adapters/mysql/test_adk_store.py index 41ba6344b..79f35ad69 100644 --- a/tests/integration/adapters/mysql/test_adk_store.py +++ b/tests/integration/adapters/mysql/test_adk_store.py @@ -5,7 +5,6 @@ import pytest -from sqlspec.exceptions import IntegrityError from sqlspec.utils.module_loader import import_optional_attr if TYPE_CHECKING: @@ -129,10 +128,11 @@ async def test_mysql_adk_owner_id_fk_constraint_isolation_and_cascade( fk_store, "tenant2-s1", app_name, user_id, {"tenant": "two"}, owner_id=2 ) - with pytest.raises(IntegrityError): + with pytest.raises(Exception) as exc_info: await mysql_adk_store_harness.create_session( fk_store, "invalid-fk", app_name, user_id, {"tenant": "invalid"}, owner_id=999 ) + assert any(token in str(exc_info.value).lower() for token in ("foreign key", "1452", "constraint")) tenant1_rows = await mysql_adk_store_harness.select_rows( fk_config, From ebf1cbd9aac47cfe782284c0dec4aa71df4c5d40 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 17:34:02 +0000 Subject: [PATCH 24/35] feat(dialects): add sybase and sqlanywhere sqlglot dialects with hot-path optimizations --- sqlspec/builder/_merge.py | 2 +- sqlspec/builder/_parsing_utils.py | 14 +- sqlspec/dialects/__init__.py | 5 +- sqlspec/dialects/sybase/__init__.py | 110 +++++++ sqlspec/dialects/sybase/_generators.py | 363 ++++++++++++++++++++++ sqlspec/dialects/sybase/_parsers.py | 200 ++++++++++++ sqlspec/dialects/sybase/_transforms.py | 55 ++++ tests/unit/builder/test_sybase_builder.py | 82 +++++ tests/unit/dialects/test_sqlanywhere.py | 91 ++++++ tests/unit/dialects/test_sybase.py | 129 ++++++++ 10 files changed, 1048 insertions(+), 3 deletions(-) create mode 100644 sqlspec/dialects/sybase/__init__.py create mode 100644 sqlspec/dialects/sybase/_generators.py create mode 100644 sqlspec/dialects/sybase/_parsers.py create mode 100644 sqlspec/dialects/sybase/_transforms.py create mode 100644 tests/unit/builder/test_sybase_builder.py create mode 100644 tests/unit/dialects/test_sqlanywhere.py create mode 100644 tests/unit/dialects/test_sybase.py diff --git a/sqlspec/builder/_merge.py b/sqlspec/builder/_merge.py index 6775de65e..b34279d2c 100644 --- a/sqlspec/builder/_merge.py +++ b/sqlspec/builder/_merge.py @@ -33,7 +33,7 @@ __all__ = ("Merge",) -MERGE_UNSUPPORTED_DIALECTS = frozenset({"mysql", "sqlite", "duckdb"}) +MERGE_UNSUPPORTED_DIALECTS = frozenset({"mysql", "sqlite", "duckdb", "sybase"}) _MERGE_SQL_KEYWORD_LITERALS = frozenset({"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"}) _MERGE_ARITHMETIC_NODES = (exp.Add, exp.Sub, exp.Mul, exp.Div, exp.Mod, exp.Paren) _POSTGRES_TYPE_DISPATCHER = TypeDispatcher[str]() diff --git a/sqlspec/builder/_parsing_utils.py b/sqlspec/builder/_parsing_utils.py index f4a64750f..5dc007d40 100644 --- a/sqlspec/builder/_parsing_utils.py +++ b/sqlspec/builder/_parsing_utils.py @@ -5,11 +5,13 @@ """ import contextlib +import importlib import re from collections.abc import Callable from typing import TYPE_CHECKING, Any, Final from sqlglot import exp, maybe_parse +from sqlglot.dialects.dialect import Dialect from sqlspec.builder._column import Column from sqlspec.builder._expression_wrappers import ExpressionWrapper @@ -437,7 +439,17 @@ def _normalize_dialect(dialect: "DialectType | None") -> str | None: name = dialect.__name__.lower() else: name = type(dialect).__name__.lower() - return {"mssql": "tsql", "mariadb": "mysql", "cockroachdb": "postgres", "postgresql": "postgres"}.get(name, name) + resolved = { + "mssql": "tsql", + "mariadb": "mysql", + "cockroachdb": "postgres", + "postgresql": "postgres", + "ase": "sybase", + "sa": "sqlanywhere", + }.get(name, name) + if resolved in {"sybase", "sqlanywhere"} and resolved not in Dialect.classes: + importlib.import_module("sqlspec.dialects.sybase") + return resolved def _merge_sql_parameters(sql_obj: Any, builder: Any) -> dict[str, str]: diff --git a/sqlspec/dialects/__init__.py b/sqlspec/dialects/__init__.py index 05d2a2acb..8ce8a02f1 100644 --- a/sqlspec/dialects/__init__.py +++ b/sqlspec/dialects/__init__.py @@ -17,16 +17,19 @@ from sqlspec.dialects.db2 import DB2 from sqlspec.dialects.postgres import ParadeDB, PGTextSearch, PGVector from sqlspec.dialects.spanner import Spangres, Spanner + from sqlspec.dialects.sybase import SQLAnywhere, Sybase -__all__ = ("DB2", "PGTextSearch", "PGVector", "ParadeDB", "Spangres", "Spanner") +__all__ = ("DB2", "PGTextSearch", "PGVector", "ParadeDB", "SQLAnywhere", "Spangres", "Spanner", "Sybase") _DIALECT_MODULES = { "DB2": "sqlspec.dialects.db2", "PGTextSearch": "sqlspec.dialects.postgres", "PGVector": "sqlspec.dialects.postgres", "ParadeDB": "sqlspec.dialects.postgres", + "SQLAnywhere": "sqlspec.dialects.sybase", "Spangres": "sqlspec.dialects.spanner", "Spanner": "sqlspec.dialects.spanner", + "Sybase": "sqlspec.dialects.sybase", } diff --git a/sqlspec/dialects/sybase/__init__.py b/sqlspec/dialects/sybase/__init__.py new file mode 100644 index 000000000..264f45a38 --- /dev/null +++ b/sqlspec/dialects/sybase/__init__.py @@ -0,0 +1,110 @@ +"""Sybase ASE and SAP SQL Anywhere sqlglot dialects. + +Both dialects inherit from sqlglot's ``TSQL`` dialect with +``NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_SENSITIVE`` and install +per-instance generator dispatch overlays (``sybase_dispatch`` and +``sqlanywhere_dispatch``) so shared sqlglot generator classes are never +mutated or subclassed. +""" + +from typing import TYPE_CHECKING, Any + +from sqlglot import TokenType, exp, generator +from sqlglot.dialects.dialect import Dialect, NormalizationStrategy +from sqlglot.dialects.tsql import TSQL + +from sqlspec.dialects.sybase._generators import sqlanywhere_dispatch, sybase_dispatch +from sqlspec.dialects.sybase._parsers import ( + normalize_sqlanywhere_expression, + normalize_sqlanywhere_tokens, + normalize_sybase_expression, + normalize_sybase_tokens, +) + +if TYPE_CHECKING: + from sqlglot.tokenizer_core import Token + +__all__ = ("SQLAnywhere", "SQLAnywhereTokenizer", "Sybase", "SybaseTokenizer") + +_DISPATCH_ATTR = "_dispatch" + + +class SybaseTokenizer(TSQL.Tokenizer): + """Tokenizer for SAP Adaptive Server Enterprise (Sybase ASE) SQL syntax.""" + + KEYWORDS = { + **TSQL.Tokenizer.KEYWORDS, + "BIGDATETIME": TokenType.DATETIME, + "BIGTIME": TokenType.TIME, + "BLOB": TokenType.BLOB, + } + + def tokenize(self, sql: str) -> "list[Token]": + """Tokenize Sybase ASE SQL and normalize ASE-only token sequences.""" + return normalize_sybase_tokens(super().tokenize(sql)) + + +class Sybase(TSQL): + """SAP Adaptive Server Enterprise (Sybase ASE) SQL dialect.""" + + NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_SENSITIVE + + Tokenizer = SybaseTokenizer + Generator = TSQL.Generator + + def generator(self, **opts: Any) -> "generator.Generator": + """Return a generator whose dispatch table carries the Sybase ASE handlers.""" + instance = TSQL.Generator(dialect=self, **opts) + base_dispatch = getattr(instance, _DISPATCH_ATTR) + setattr(instance, _DISPATCH_ATTR, sybase_dispatch(base_dispatch)) + return instance + + def parse(self, sql: str, **opts: Any) -> "list[exp.Expr | None]": + """Parse Sybase ASE SQL and normalize ASE function nodes.""" + parsed = self.parser(**opts).parse(self.tokenize(sql), sql) + return [normalize_sybase_expression(expr) if expr is not None else None for expr in parsed] + + def parse_into(self, expression_type: Any, sql: str, **opts: Any) -> "list[exp.Expr | None]": + """Parse Sybase ASE SQL into a target expression type.""" + parsed = self.parser(**opts).parse_into(expression_type, self.tokenize(sql), sql) + return [normalize_sybase_expression(expr) if expr is not None else None for expr in parsed] + + +class SQLAnywhereTokenizer(TSQL.Tokenizer): + """Tokenizer for SAP SQL Anywhere SQL syntax.""" + + KEYWORDS = {**TSQL.Tokenizer.KEYWORDS, "BLOB": TokenType.BLOB, "UNIQUEIDENTIFIER": TokenType.UUID} + + def tokenize(self, sql: str) -> "list[Token]": + """Tokenize SAP SQL Anywhere SQL and normalize SQL Anywhere token sequences.""" + return normalize_sqlanywhere_tokens(super().tokenize(sql)) + + +class SQLAnywhere(TSQL): + """SAP SQL Anywhere SQL dialect.""" + + NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_SENSITIVE + + Tokenizer = SQLAnywhereTokenizer + Generator = TSQL.Generator + + def generator(self, **opts: Any) -> "generator.Generator": + """Return a generator whose dispatch table carries the SAP SQL Anywhere handlers.""" + instance = TSQL.Generator(dialect=self, **opts) + base_dispatch = getattr(instance, _DISPATCH_ATTR) + setattr(instance, _DISPATCH_ATTR, sqlanywhere_dispatch(base_dispatch)) + return instance + + def parse(self, sql: str, **opts: Any) -> "list[exp.Expr | None]": + """Parse SAP SQL Anywhere SQL and normalize SQL Anywhere function nodes.""" + parsed = self.parser(**opts).parse(self.tokenize(sql), sql) + return [normalize_sqlanywhere_expression(expr) if expr is not None else None for expr in parsed] + + def parse_into(self, expression_type: Any, sql: str, **opts: Any) -> "list[exp.Expr | None]": + """Parse SAP SQL Anywhere SQL into a target expression type.""" + parsed = self.parser(**opts).parse_into(expression_type, self.tokenize(sql), sql) + return [normalize_sqlanywhere_expression(expr) if expr is not None else None for expr in parsed] + + +Dialect.classes["sybase"] = Sybase +Dialect.classes["sqlanywhere"] = SQLAnywhere diff --git a/sqlspec/dialects/sybase/_generators.py b/sqlspec/dialects/sybase/_generators.py new file mode 100644 index 000000000..abad841f9 --- /dev/null +++ b/sqlspec/dialects/sybase/_generators.py @@ -0,0 +1,363 @@ +"""Sybase ASE and SAP SQL Anywhere render handlers layered onto per-instance sqlglot generator dispatch tables.""" + +from collections.abc import Callable +from typing import Any, Final, cast + +from sqlglot import exp, generator + +from sqlspec.dialects.sybase._transforms import ( + clone_with_detached_pagination, + is_literal_integer_limit, + render_char_length, + render_charindex, + render_ilike, +) + +__all__ = ( + "SQLANYWHERE_DEFAULT_TYPE_LENGTHS", + "SQLANYWHERE_TRANSFORMS", + "SQLANYWHERE_TYPE_MAPPING", + "SYBASE_DEFAULT_TYPE_LENGTHS", + "SYBASE_TRANSFORMS", + "SYBASE_TYPE_MAPPING", + "char_length_sql", + "charindex_sql", + "ilike_sql", + "sqlanywhere_create_sql", + "sqlanywhere_current_timestamp_sql", + "sqlanywhere_datatype_sql", + "sqlanywhere_dispatch", + "sqlanywhere_drop_sql", + "sqlanywhere_select_sql", + "sqlanywhere_set_operation_sql", + "sybase_autoincrement_sql", + "sybase_columndef_sql", + "sybase_current_timestamp_sql", + "sybase_datatype_sql", + "sybase_dispatch", + "sybase_generated_identity_sql", + "sybase_returning_sql", + "sybase_select_sql", + "sybase_set_operation_sql", +) + +SYBASE_TYPE_MAPPING: Final[dict[exp.DType, str]] = { + **generator.Generator.TYPE_MAPPING, + exp.DType.BOOLEAN: "BIT", + exp.DType.BIT: "BIT", + exp.DType.BLOB: "IMAGE", + exp.DType.IMAGE: "IMAGE", + exp.DType.TEXT: "TEXT", + exp.DType.JSON: "TEXT", + exp.DType.JSONB: "TEXT", + exp.DType.UUID: "VARCHAR(36)", + exp.DType.VARCHAR: "VARCHAR", + exp.DType.NVARCHAR: "UNIVARCHAR", + exp.DType.CHAR: "CHAR", + exp.DType.NCHAR: "UNICHAR", + exp.DType.BINARY: "BINARY", + exp.DType.VARBINARY: "VARBINARY", + exp.DType.TIMESTAMP: "BIGDATETIME", + exp.DType.TIMESTAMPTZ: "BIGDATETIME", + exp.DType.TIMESTAMPLTZ: "BIGDATETIME", + exp.DType.TIMESTAMPNTZ: "BIGDATETIME", + exp.DType.DATETIME: "BIGDATETIME", + exp.DType.DATETIME2: "BIGDATETIME", + exp.DType.DATE: "DATE", + exp.DType.TIME: "BIGTIME", + exp.DType.TIMETZ: "BIGTIME", + exp.DType.TINYINT: "TINYINT", + exp.DType.SMALLINT: "SMALLINT", + exp.DType.INT: "INT", + exp.DType.BIGINT: "BIGINT", + exp.DType.FLOAT: "FLOAT", + exp.DType.DOUBLE: "DOUBLE PRECISION", + exp.DType.DECIMAL: "DECIMAL", +} + +SYBASE_DEFAULT_TYPE_LENGTHS: Final[dict[exp.DType, str]] = { + exp.DType.VARCHAR: "VARCHAR(16384)", + exp.DType.NVARCHAR: "UNIVARCHAR(8192)", + exp.DType.VARBINARY: "VARBINARY(16384)", +} + +SQLANYWHERE_TYPE_MAPPING: Final[dict[exp.DType, str]] = { + **generator.Generator.TYPE_MAPPING, + exp.DType.BOOLEAN: "BIT", + exp.DType.BIT: "BIT", + exp.DType.BLOB: "LONG BINARY", + exp.DType.IMAGE: "LONG BINARY", + exp.DType.TEXT: "LONG VARCHAR", + exp.DType.JSON: "LONG VARCHAR", + exp.DType.JSONB: "LONG VARCHAR", + exp.DType.UUID: "UNIQUEIDENTIFIER", + exp.DType.VARCHAR: "VARCHAR", + exp.DType.NVARCHAR: "NVARCHAR", + exp.DType.CHAR: "CHAR", + exp.DType.NCHAR: "NCHAR", + exp.DType.BINARY: "BINARY", + exp.DType.VARBINARY: "VARBINARY", + exp.DType.TIMESTAMP: "TIMESTAMP", + exp.DType.TIMESTAMPNTZ: "TIMESTAMP", + exp.DType.DATETIME: "TIMESTAMP", + exp.DType.DATETIME2: "TIMESTAMP", + exp.DType.TIMESTAMPTZ: "TIMESTAMP WITH TIME ZONE", + exp.DType.TIMESTAMPLTZ: "TIMESTAMP WITH TIME ZONE", + exp.DType.DATE: "DATE", + exp.DType.TIME: "TIME", + exp.DType.TIMETZ: "TIME", + exp.DType.TINYINT: "TINYINT", + exp.DType.SMALLINT: "SMALLINT", + exp.DType.INT: "INT", + exp.DType.BIGINT: "BIGINT", + exp.DType.FLOAT: "DOUBLE", + exp.DType.DOUBLE: "DOUBLE", + exp.DType.DECIMAL: "DECIMAL", +} + +SQLANYWHERE_DEFAULT_TYPE_LENGTHS: Final[dict[exp.DType, str]] = { + exp.DType.VARCHAR: "VARCHAR(32767)", + exp.DType.NVARCHAR: "NVARCHAR(32767)", + exp.DType.VARBINARY: "VARBINARY(32767)", +} + +_BOOLEAN_TYPES: Final[frozenset[exp.DType]] = frozenset({exp.DType.BOOLEAN, exp.DType.BIT}) + + +def sybase_select_sql(gen: "generator.Generator", expression: exp.Select) -> str: + """Render a Sybase ASE SELECT with copy-on-write TOP or ROWS LIMIT/OFFSET pagination.""" + limit_arg = expression.args.get("limit") + offset_arg = expression.args.get("offset") + if offset_arg is None and is_literal_integer_limit(limit_arg): + cloned = expression.copy() + return generator.Generator.select_sql(gen, cloned) + detached, limit_node, offset_node = clone_with_detached_pagination(expression) + base_sql = generator.Generator.select_sql(gen, cast("exp.Select", detached)) + tail = _render_sybase_pagination_tail(gen, limit_node, offset_node) + return f"{base_sql} {tail}" if tail else base_sql + + +def sybase_set_operation_sql(gen: "generator.Generator", expression: exp.SetOperation) -> str: + """Render Sybase ASE set operations with copy-on-write ROWS LIMIT/OFFSET pagination.""" + detached, limit_node, offset_node = clone_with_detached_pagination(expression) + base_sql = gen.set_operations(cast("exp.SetOperation", detached)) + tail = _render_sybase_pagination_tail(gen, limit_node, offset_node) + return f"{base_sql} {tail}" if tail else base_sql + + +def sybase_datatype_sql(gen: "generator.Generator", expression: exp.DataType) -> str: + """Render a data type using Sybase ASE type names and MAX / nullable BIT rules.""" + if _is_max_length_datatype(expression): + if expression.this in {exp.DType.NVARCHAR, exp.DType.NCHAR}: + return "UNITEXT" + if expression.this in {exp.DType.VARBINARY, exp.DType.BINARY, exp.DType.BLOB}: + return "IMAGE" + return "TEXT" + if expression.this in _BOOLEAN_TYPES and _is_explicitly_nullable_columndef(expression.parent): + return "TINYINT" + type_str = SYBASE_TYPE_MAPPING.get(expression.this) + if not type_str: + return generator.Generator.datatype_sql(gen, expression) + if expression.expressions: + params = ", ".join(gen.sql(e) for e in expression.expressions) + return f"{type_str}({params})" + return SYBASE_DEFAULT_TYPE_LENGTHS.get(expression.this, type_str) + + +def sybase_columndef_sql(gen: "generator.Generator", expression: exp.ColumnDef) -> str: + """Render a Sybase ASE column definition.""" + return generator.Generator.columndef_sql(gen, expression) + + +def sybase_generated_identity_sql( + gen: "generator.Generator", expression: exp.GeneratedAsIdentityColumnConstraint +) -> str: + """Render GeneratedAsIdentityColumnConstraint as bare Sybase ASE IDENTITY.""" + _ = (gen, expression) + return "IDENTITY" + + +def sybase_autoincrement_sql(gen: "generator.Generator", expression: exp.AutoIncrementColumnConstraint) -> str: + """Render AutoIncrementColumnConstraint as bare Sybase ASE IDENTITY.""" + _ = (gen, expression) + return "IDENTITY" + + +def sybase_current_timestamp_sql(gen: "generator.Generator", expression: exp.CurrentTimestamp) -> str: + """Render CurrentTimestamp as GETUTCDATE().""" + _ = (gen, expression) + return "GETUTCDATE()" + + +def sybase_returning_sql(gen: "generator.Generator", expression: exp.Returning) -> str: + """Omit RETURNING / OUTPUT clauses in Sybase ASE.""" + _ = expression + gen.unsupported("Sybase ASE does not support RETURNING or OUTPUT clauses") + return "" + + +def sqlanywhere_select_sql(gen: "generator.Generator", expression: exp.Select) -> str: + """Render a SAP SQL Anywhere SELECT with copy-on-write trailing LIMIT/OFFSET pagination.""" + detached, limit_node, offset_node = clone_with_detached_pagination(expression) + base_sql = generator.Generator.select_sql(gen, cast("exp.Select", detached)) + tail = _render_sqlanywhere_pagination_tail(gen, limit_node, offset_node) + return f"{base_sql} {tail}" if tail else base_sql + + +def sqlanywhere_set_operation_sql(gen: "generator.Generator", expression: exp.SetOperation) -> str: + """Render SAP SQL Anywhere set operations with copy-on-write trailing LIMIT/OFFSET pagination.""" + detached, limit_node, offset_node = clone_with_detached_pagination(expression) + base_sql = gen.set_operations(cast("exp.SetOperation", detached)) + tail = _render_sqlanywhere_pagination_tail(gen, limit_node, offset_node) + return f"{base_sql} {tail}" if tail else base_sql + + +def sqlanywhere_datatype_sql(gen: "generator.Generator", expression: exp.DataType) -> str: + """Render a data type using SAP SQL Anywhere type names.""" + if _is_max_length_datatype(expression): + if expression.this in {exp.DType.NVARCHAR, exp.DType.NCHAR}: + return "LONG NVARCHAR" + if expression.this in {exp.DType.VARBINARY, exp.DType.BINARY, exp.DType.BLOB}: + return "LONG BINARY" + return "LONG VARCHAR" + type_str = SQLANYWHERE_TYPE_MAPPING.get(expression.this) + if not type_str: + return generator.Generator.datatype_sql(gen, expression) + if expression.expressions: + params = ", ".join(gen.sql(e) for e in expression.expressions) + return f"{type_str}({params})" + return SQLANYWHERE_DEFAULT_TYPE_LENGTHS.get(expression.this, type_str) + + +def sqlanywhere_create_sql(gen: "generator.Generator", expression: exp.Create) -> str: + """Render CREATE statements using native SQL Anywhere IF NOT EXISTS syntax.""" + return generator.Generator.create_sql(gen, expression) + + +def sqlanywhere_drop_sql(gen: "generator.Generator", expression: exp.Drop) -> str: + """Render DROP statements using native SQL Anywhere IF EXISTS syntax.""" + return generator.Generator.drop_sql(gen, expression) + + +def sqlanywhere_current_timestamp_sql(gen: "generator.Generator", expression: exp.CurrentTimestamp) -> str: + """Render CurrentTimestamp as CURRENT UTC TIMESTAMP.""" + _ = (gen, expression) + return "CURRENT UTC TIMESTAMP" + + +def char_length_sql(gen: "generator.Generator", expression: exp.Length) -> str: + """Render exp.Length as CHAR_LENGTH(this).""" + return render_char_length(gen, expression) + + +def charindex_sql(gen: "generator.Generator", expression: exp.StrPosition) -> str: + """Render exp.StrPosition as CHARINDEX(substr, this[, position]).""" + return render_charindex(gen, expression) + + +def ilike_sql(gen: "generator.Generator", expression: exp.ILike) -> str: + """Render exp.ILike as LOWER(this) LIKE LOWER(expression).""" + return render_ilike(gen, expression) + + +SYBASE_TRANSFORMS: Final[dict[type[exp.Expr], Callable[[Any, Any], str]]] = { + exp.Select: sybase_select_sql, + exp.Union: sybase_set_operation_sql, + exp.Intersect: sybase_set_operation_sql, + exp.Except: sybase_set_operation_sql, + exp.DataType: sybase_datatype_sql, + exp.ColumnDef: sybase_columndef_sql, + exp.GeneratedAsIdentityColumnConstraint: sybase_generated_identity_sql, + exp.AutoIncrementColumnConstraint: sybase_autoincrement_sql, + exp.CurrentTimestamp: sybase_current_timestamp_sql, + exp.Returning: sybase_returning_sql, + exp.Length: char_length_sql, + exp.StrPosition: charindex_sql, + exp.ILike: ilike_sql, +} + +SQLANYWHERE_TRANSFORMS: Final[dict[type[exp.Expr], Callable[[Any, Any], str]]] = { + exp.Select: sqlanywhere_select_sql, + exp.Union: sqlanywhere_set_operation_sql, + exp.Intersect: sqlanywhere_set_operation_sql, + exp.Except: sqlanywhere_set_operation_sql, + exp.DataType: sqlanywhere_datatype_sql, + exp.Create: sqlanywhere_create_sql, + exp.Drop: sqlanywhere_drop_sql, + exp.CurrentTimestamp: sqlanywhere_current_timestamp_sql, + exp.Length: char_length_sql, + exp.StrPosition: charindex_sql, + exp.ILike: ilike_sql, +} + +_sybase_overlay_cache: "tuple[dict[type[exp.Expr], Callable[..., str]], dict[type[exp.Expr], Callable[..., str]]] | None" = None +_sqlanywhere_overlay_cache: "tuple[dict[type[exp.Expr], Callable[..., str]], dict[type[exp.Expr], Callable[..., str]]] | None" = None + + +def sybase_dispatch(base: "dict[type[exp.Expr], Callable[..., str]]") -> "dict[type[exp.Expr], Callable[..., str]]": + """Return the Sybase ASE dispatch table layered over a root generator dispatch table.""" + global _sybase_overlay_cache + cache = _sybase_overlay_cache + if cache is not None and cache[0] is base: + return cache[1] + overlay = {**base, **SYBASE_TRANSFORMS} + _sybase_overlay_cache = (base, overlay) + return overlay + + +def sqlanywhere_dispatch( + base: "dict[type[exp.Expr], Callable[..., str]]", +) -> "dict[type[exp.Expr], Callable[..., str]]": + """Return the SAP SQL Anywhere dispatch table layered over a root generator dispatch table.""" + global _sqlanywhere_overlay_cache + cache = _sqlanywhere_overlay_cache + if cache is not None and cache[0] is base: + return cache[1] + overlay = {**base, **SQLANYWHERE_TRANSFORMS} + _sqlanywhere_overlay_cache = (base, overlay) + return overlay + + +def _is_max_length_datatype(expression: exp.DataType) -> bool: + if len(expression.expressions) != 1: + return False + first = expression.expressions[0] + return str(first).upper() == "MAX" or (isinstance(first, exp.DataTypeParam) and str(first.this).upper() == "MAX") + + +def _is_explicitly_nullable_columndef(parent: exp.Expr | None) -> bool: + if not isinstance(parent, exp.ColumnDef): + return False + for constraint in parent.constraints: + kind = constraint.kind + if isinstance(kind, exp.NotNullColumnConstraint) and bool(kind.args.get("allow_null")): + return True + return False + + +def _render_sybase_pagination_tail( + gen: "generator.Generator", limit_node: exp.Limit | None, offset_node: exp.Offset | None +) -> str: + limit_sql = gen.sql(limit_node, "expression") if limit_node is not None else "" + offset_sql = gen.sql(offset_node, "expression") if offset_node is not None else "" + if limit_sql and offset_sql: + return f"ROWS LIMIT {limit_sql} OFFSET {offset_sql}" + if limit_sql: + return f"ROWS LIMIT {limit_sql}" + if offset_sql: + return f"ROWS OFFSET {offset_sql}" + return "" + + +def _render_sqlanywhere_pagination_tail( + gen: "generator.Generator", limit_node: exp.Limit | None, offset_node: exp.Offset | None +) -> str: + limit_sql = gen.sql(limit_node, "expression") if limit_node is not None else "" + offset_sql = gen.sql(offset_node, "expression") if offset_node is not None else "" + if limit_sql and offset_sql: + return f"LIMIT {limit_sql} OFFSET {offset_sql}" + if limit_sql: + return f"LIMIT {limit_sql}" + if offset_sql: + return f"OFFSET {offset_sql}" + return "" diff --git a/sqlspec/dialects/sybase/_parsers.py b/sqlspec/dialects/sybase/_parsers.py new file mode 100644 index 000000000..4ad5347dc --- /dev/null +++ b/sqlspec/dialects/sybase/_parsers.py @@ -0,0 +1,200 @@ +"""Sybase ASE and SAP SQL Anywhere token and AST normalization helpers.""" + +from typing import Final + +from sqlglot import exp +from sqlglot.tokenizer_core import Token, TokenType + +__all__ = ( + "normalize_sqlanywhere_expression", + "normalize_sqlanywhere_tokens", + "normalize_sybase_expression", + "normalize_sybase_tokens", +) + +_QUOTED_TOKEN_TYPES: Final[frozenset[TokenType]] = frozenset({ + TokenType.STRING, + TokenType.IDENTIFIER, + TokenType.NATIONAL_STRING, + TokenType.RAW_STRING, + TokenType.BYTE_STRING, + TokenType.HEX_STRING, + TokenType.BIT_STRING, + TokenType.HEREDOC_STRING, + TokenType.UNICODE_STRING, +}) +_CHARINDEX_MIN_ARGS: Final[int] = 2 +_CHARINDEX_MAX_ARGS: Final[int] = 3 +_TOP_START_AT_SPAN: Final[int] = 5 + + +def normalize_sybase_tokens(tokens: "list[Token]") -> "list[Token]": + """Rewrite Sybase ASE token sequences into forms accepted by the T-SQL parser. + + Trailing ``ROWS LIMIT [OFFSET ]`` and ``ROWS OFFSET `` sequences + have their leading ``ROWS`` token removed so sqlglot parses them into + canonical ``exp.Limit`` and ``exp.Offset`` nodes, and ``UNITEXT`` tokens + are expanded to ``NVARCHAR(MAX)`` for round-trip preservation. + """ + result: list[Token] = [] + index = 0 + total = len(tokens) + while index < total: + token = tokens[index] + if ( + token.token_type == TokenType.ROWS + and index + 1 < total + and tokens[index + 1].token_type in {TokenType.LIMIT, TokenType.OFFSET} + ): + index += 1 + continue + if token.token_type not in _QUOTED_TOKEN_TYPES and token.text.upper() == "UNITEXT": + result.extend(_expand_nvarchar_max(token)) + index += 1 + continue + result.append(token) + index += 1 + return result + + +def normalize_sqlanywhere_tokens(tokens: "list[Token]") -> "list[Token]": + """Rewrite SAP SQL Anywhere token sequences into forms accepted by the T-SQL parser. + + Normalizes ``LONG VARCHAR``, ``LONG NVARCHAR``, ``LONG BINARY``, and + ``CURRENT UTC TIMESTAMP`` tokens, and rewrites Watcom-SQL + ``SELECT [DISTINCT|ALL] TOP START AT ...`` headers into trailing + 0-based ``LIMIT OFFSET `` clauses. + """ + rewritten = _normalize_sqlanywhere_multiword_tokens(tokens) + result: list[Token] = [] + statement: list[Token] = [] + for token in rewritten: + if token.token_type == TokenType.SEMICOLON: + result.extend(_rewrite_top_start_at_statement(statement)) + result.append(token) + statement = [] + else: + statement.append(token) + result.extend(_rewrite_top_start_at_statement(statement)) + return result + + +def normalize_sybase_expression(expression: exp.Expr) -> exp.Expr: + """Normalize Sybase ASE function nodes into canonical sqlglot AST expressions.""" + return expression.transform(_canonical_sybase_node, copy=False) + + +def normalize_sqlanywhere_expression(expression: exp.Expr) -> exp.Expr: + """Normalize SAP SQL Anywhere function nodes into canonical sqlglot AST expressions.""" + return expression.transform(_canonical_sybase_node, copy=False) + + +def _expand_nvarchar_max(token: Token) -> "list[Token]": + return [ + Token(TokenType.NVARCHAR, "NVARCHAR", token.line, token.col, token.start, token.end, token.comments), + Token(TokenType.L_PAREN, "(", token.line, token.col, token.start, token.end), + Token(TokenType.VAR, "MAX", token.line, token.col, token.start, token.end), + Token(TokenType.R_PAREN, ")", token.line, token.col, token.start, token.end), + ] + + +def _normalize_sqlanywhere_multiword_tokens(tokens: "list[Token]") -> "list[Token]": + result: list[Token] = [] + index = 0 + total = len(tokens) + while index < total: + token = tokens[index] + if token.token_type not in _QUOTED_TOKEN_TYPES: + upper = token.text.upper() + if upper == "LONG" and index + 1 < total and tokens[index + 1].token_type not in _QUOTED_TOKEN_TYPES: + second_upper = tokens[index + 1].text.upper() + if second_upper == "VARCHAR": + result.append( + Token(TokenType.TEXT, "TEXT", token.line, token.col, token.start, tokens[index + 1].end) + ) + index += 2 + continue + if second_upper == "NVARCHAR": + result.extend(_expand_nvarchar_max(token)) + index += 2 + continue + if second_upper == "BINARY": + result.append( + Token(TokenType.BLOB, "BLOB", token.line, token.col, token.start, tokens[index + 1].end) + ) + index += 2 + continue + if ( + upper == "CURRENT" + and index + 2 < total + and tokens[index + 1].token_type not in _QUOTED_TOKEN_TYPES + and tokens[index + 2].token_type not in _QUOTED_TOKEN_TYPES + and tokens[index + 1].text.upper() == "UTC" + and tokens[index + 2].text.upper() in {"TIMESTAMP", "CURRENT_TIMESTAMP"} + ): + result.append( + Token( + TokenType.CURRENT_TIMESTAMP, + "CURRENT_TIMESTAMP", + token.line, + token.col, + token.start, + tokens[index + 2].end, + ) + ) + index += 3 + continue + result.append(token) + index += 1 + return result + + +def _rewrite_top_start_at_statement(statement: "list[Token]") -> "list[Token]": + if len(statement) < _TOP_START_AT_SPAN + 1 or statement[0].token_type != TokenType.SELECT: + return statement + header_index = 1 + if statement[header_index].token_type in {TokenType.DISTINCT, TokenType.ALL}: + header_index += 1 + if header_index + _TOP_START_AT_SPAN > len(statement): + return statement + top_tok = statement[header_index] + limit_tok = statement[header_index + 1] + start_word_tok = statement[header_index + 2] + at_word_tok = statement[header_index + 3] + start_val_tok = statement[header_index + 4] + if ( + top_tok.token_type != TokenType.TOP + or start_word_tok.token_type in _QUOTED_TOKEN_TYPES + or at_word_tok.token_type in _QUOTED_TOKEN_TYPES + or start_word_tok.text.upper() != "START" + or at_word_tok.text.upper() != "AT" + ): + return statement + if start_val_tok.token_type == TokenType.NUMBER and start_val_tok.text.isdigit(): + offset_str = str(max(int(start_val_tok.text) - 1, 0)) + offset_tok = Token( + TokenType.NUMBER, offset_str, start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end + ) + else: + offset_tok = start_val_tok + last = statement[-1] + tail = [ + Token(TokenType.LIMIT, "LIMIT", last.line, last.col, last.end, last.end), + limit_tok, + Token(TokenType.OFFSET, "OFFSET", last.line, last.col, last.end, last.end), + offset_tok, + ] + return [*statement[:header_index], *statement[header_index + _TOP_START_AT_SPAN :], *tail] + + +def _canonical_sybase_node(node: exp.Expr) -> exp.Expr: + if isinstance(node, exp.Anonymous): + func_name = str(node.this).upper() + if func_name == "CHAR_LENGTH" and len(node.expressions) == 1: + return exp.Length(this=node.expressions[0]) + if func_name == "CHARINDEX" and _CHARINDEX_MIN_ARGS <= len(node.expressions) <= _CHARINDEX_MAX_ARGS: + substr = node.expressions[0] + this = node.expressions[1] + position = node.expressions[2] if len(node.expressions) == _CHARINDEX_MAX_ARGS else None + return exp.StrPosition(this=this, substr=substr, position=position) + return node diff --git a/sqlspec/dialects/sybase/_transforms.py b/sqlspec/dialects/sybase/_transforms.py new file mode 100644 index 000000000..0b09f2532 --- /dev/null +++ b/sqlspec/dialects/sybase/_transforms.py @@ -0,0 +1,55 @@ +"""Shared AST transformation and rendering helpers for Sybase ASE and SAP SQL Anywhere dialects.""" + +from typing import Any + +from sqlglot import exp + +__all__ = ( + "clone_with_detached_pagination", + "is_literal_integer_limit", + "render_char_length", + "render_charindex", + "render_ilike", +) + + +def is_literal_integer_limit(limit_expr: Any) -> bool: + """Return True when limit_expr is a literal integer exp.Limit without parameters.""" + if not isinstance(limit_expr, exp.Limit): + return False + count_expr = limit_expr.expression + return isinstance(count_expr, exp.Literal) and not count_expr.is_string + + +def clone_with_detached_pagination(expression: exp.Expr) -> "tuple[exp.Expr, exp.Limit | None, exp.Offset | None]": + """Return a shallow copy of expression with limit and offset removed if present.""" + limit_arg = expression.args.get("limit") + offset_arg = expression.args.get("offset") + if limit_arg is None and offset_arg is None: + return expression, None, None + cloned = expression.copy() + cloned.set("limit", None) + cloned.set("offset", None) + limit_node = limit_arg if isinstance(limit_arg, exp.Limit) else None + offset_node = offset_arg if isinstance(offset_arg, exp.Offset) else None + return cloned, limit_node, offset_node + + +def render_char_length(generator: Any, expression: exp.Length) -> str: + """Render exp.Length as CHAR_LENGTH(this).""" + return f"CHAR_LENGTH({generator.sql(expression, 'this')})" + + +def render_charindex(generator: Any, expression: exp.StrPosition) -> str: + """Render exp.StrPosition as CHARINDEX(substr, this[, position]).""" + this_sql = generator.sql(expression, "this") + substr_sql = generator.sql(expression, "substr") + position = expression.args.get("position") + if position is not None: + return f"CHARINDEX({substr_sql}, {this_sql}, {generator.sql(position)})" + return f"CHARINDEX({substr_sql}, {this_sql})" + + +def render_ilike(generator: Any, expression: exp.ILike) -> str: + """Render case-insensitive ILIKE using LOWER(this) LIKE LOWER(expression).""" + return f"LOWER({generator.sql(expression, 'this')}) LIKE LOWER({generator.sql(expression, 'expression')})" diff --git a/tests/unit/builder/test_sybase_builder.py b/tests/unit/builder/test_sybase_builder.py new file mode 100644 index 000000000..936161145 --- /dev/null +++ b/tests/unit/builder/test_sybase_builder.py @@ -0,0 +1,82 @@ +"""Unit tests for SQLSpec QueryBuilder with Sybase ASE and SAP SQL Anywhere dialects.""" + +import pytest + +from sqlspec import sql +from sqlspec.core import get_cache +from sqlspec.exceptions import DialectNotSupportedError + +__all__ = () + + +def test_sybase_builder_select_limit_offset_and_optimizer_cache() -> None: + """Verify QueryBuilder with dialect='sybase' renders ROWS LIMIT/OFFSET and populates QueryCache.""" + cache = get_cache() + cache.clear() + + builder = ( + sql + .select("id", "name", dialect="sybase") + .from_("users") + .where_eq("status", "active") + .order_by("id") + .limit(10) + .offset(20) + ) + first = builder.build() + second = builder.build() + + assert "ROWS LIMIT" in first.sql + assert "OFFSET" in first.sql + assert first.sql == second.sql + assert cache.get_stats().hits >= 1 + + +def test_sqlanywhere_builder_select_limit_offset_and_optimizer_cache() -> None: + """Verify QueryBuilder with dialect='sqlanywhere' renders LIMIT/OFFSET and populates QueryCache.""" + cache = get_cache() + cache.clear() + + builder = ( + sql + .select("id", "name", dialect="sqlanywhere") + .from_("users") + .where_eq("status", "active") + .order_by("id") + .limit(10) + .offset(20) + ) + first = builder.build() + second = builder.build() + + assert "LIMIT" in first.sql + assert "OFFSET" in first.sql + assert "FETCH" not in first.sql + assert first.sql == second.sql + assert cache.get_stats().hits >= 1 + + +def test_sybase_merge_raises_dialect_not_supported() -> None: + """Verify sql.merge(dialect='sybase').build() raises DialectNotSupportedError while sqlanywhere is supported.""" + with pytest.raises(DialectNotSupportedError): + ( + sql + .merge(dialect="sybase") + .into("users", alias="t") + .using("staging_users", alias="s") + .on("t.id = s.id") + .when_matched_then_update(name="s.name") + .build() + ) + + merge_builder = ( + sql + .merge(dialect="sqlanywhere") + .into("users", alias="t") + .using("staging_users", alias="s") + .on("t.id = s.id") + .when_matched_then_update(name="s.name") + .when_not_matched_then_insert(id="s.id", name="s.name") + ) + compiled = merge_builder.build() + assert "MERGE INTO" in compiled.sql diff --git a/tests/unit/dialects/test_sqlanywhere.py b/tests/unit/dialects/test_sqlanywhere.py new file mode 100644 index 000000000..2d509a93f --- /dev/null +++ b/tests/unit/dialects/test_sqlanywhere.py @@ -0,0 +1,91 @@ +"""Unit tests for the custom SAP SQL Anywhere sqlglot dialect.""" + +import sqlglot +from sqlglot import exp, parse_one, transpile +from sqlglot.dialects.dialect import NormalizationStrategy + +from sqlspec.dialects.sybase import SQLAnywhere, SQLAnywhereTokenizer + +__all__ = () + + +def test_sqlanywhere_dialect_registration() -> None: + """Verify SQLAnywhere dialect is registered and resolvable by name.""" + dialect = sqlglot.Dialect.get_or_raise("sqlanywhere") + assert dialect.__class__ is SQLAnywhere + assert dialect.tokenizer_class is SQLAnywhereTokenizer + assert dialect.NORMALIZATION_STRATEGY == NormalizationStrategy.CASE_SENSITIVE + + +def test_sqlanywhere_limit_and_offset_rendering() -> None: + """Verify SQLAnywhere renders trailing LIMIT n and LIMIT n OFFSET m without TOP or FETCH.""" + limit_only = transpile("SELECT id, name FROM users LIMIT 10", write="sqlanywhere")[0] + assert limit_only == "SELECT id, name FROM users LIMIT 10" + + limit_offset = transpile("SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 20", write="sqlanywhere")[0] + assert limit_offset == "SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 20" + + offset_only = transpile("SELECT id FROM users ORDER BY id OFFSET 5", write="sqlanywhere")[0] + assert offset_only == "SELECT id FROM users ORDER BY id OFFSET 5" + + +def test_sqlanywhere_parse_top_start_at() -> None: + """Verify Watcom-SQL SELECT TOP n START AT m parses into 0-based Limit and Offset.""" + parsed = parse_one("SELECT TOP 10 START AT 21 id, name FROM users ORDER BY id", dialect="sqlanywhere") + assert isinstance(parsed, exp.Select) + assert isinstance(parsed.args.get("limit"), exp.Limit) + assert isinstance(parsed.args.get("offset"), exp.Offset) + assert parsed.sql(dialect="sqlanywhere") == "SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 20" + + +def test_sqlanywhere_copy_false_preserves_ast_pagination() -> None: + """Verify expression.sql('sqlanywhere', copy=False) never mutates limit or offset in place.""" + select_expr = parse_one("SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 5") + first_sql = select_expr.sql(dialect="sqlanywhere", copy=False) + second_sql = select_expr.sql(dialect="sqlanywhere", copy=False) + assert first_sql == "SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 5" + assert second_sql == first_sql + assert isinstance(select_expr.args.get("limit"), exp.Limit) + assert isinstance(select_expr.args.get("offset"), exp.Offset) + + +def test_sqlanywhere_create_and_drop_if_exists() -> None: + """Verify SQLAnywhere emits native single-statement CREATE TABLE IF NOT EXISTS and DROP TABLE IF EXISTS.""" + create_sql = transpile( + "CREATE TABLE IF NOT EXISTS sessions (id VARCHAR(64) PRIMARY KEY, data TEXT)", + read="postgres", + write="sqlanywhere", + )[0] + assert create_sql == "CREATE TABLE IF NOT EXISTS sessions (id VARCHAR(64) PRIMARY KEY, data LONG VARCHAR)" + assert "OBJECT_ID" not in create_sql + + drop_sql = transpile("DROP TABLE IF EXISTS sessions", read="postgres", write="sqlanywhere")[0] + assert drop_sql == "DROP TABLE IF EXISTS sessions" + + +def test_sqlanywhere_type_mappings() -> None: + """Verify SQLAnywhere type mappings for LONG VARCHAR, LONG BINARY, TIMESTAMP, UNIQUEIDENTIFIER, and BIT.""" + assert parse_one("CAST(x AS TEXT)", read="postgres").sql("sqlanywhere") == "CAST(x AS LONG VARCHAR)" + assert parse_one("CAST(x AS JSONB)", read="postgres").sql("sqlanywhere") == "CAST(x AS LONG VARCHAR)" + assert parse_one("CAST(x AS BLOB)", dialect="sqlanywhere").sql("sqlanywhere") == "CAST(x AS LONG BINARY)" + assert parse_one("CAST(x AS BYTEA)", read="postgres").sql("sqlanywhere") == "CAST(x AS VARBINARY(32767))" + assert parse_one("CAST(x AS TIMESTAMP)", read="postgres").sql("sqlanywhere") == "CAST(x AS TIMESTAMP)" + assert parse_one("CAST(x AS TIMESTAMPTZ)", read="postgres").sql("sqlanywhere") == ( + "CAST(x AS TIMESTAMP WITH TIME ZONE)" + ) + assert parse_one("CAST(x AS UUID)", read="postgres").sql("sqlanywhere") == "CAST(x AS UNIQUEIDENTIFIER)" + assert parse_one("CAST(x AS BOOLEAN)", read="postgres").sql("sqlanywhere") == "CAST(x AS BIT)" + assert parse_one("CAST(x AS VARCHAR)", read="postgres").sql("sqlanywhere") == "CAST(x AS VARCHAR(32767))" + assert parse_one("CAST(x AS NVARCHAR(MAX))", read="tsql").sql("sqlanywhere") == "CAST(x AS LONG NVARCHAR)" + + +def test_sqlanywhere_long_types_and_utc_timestamp_parsing() -> None: + """Verify LONG VARCHAR, LONG BINARY, and CURRENT UTC TIMESTAMP parse cleanly in SQLAnywhere.""" + parsed = parse_one( + "CREATE TABLE IF NOT EXISTS docs (id UNIQUEIDENTIFIER, body LONG VARCHAR, raw_bytes LONG BINARY, created_at TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP)", + dialect="sqlanywhere", + ) + rendered = parsed.sql(dialect="sqlanywhere") + assert "LONG VARCHAR" in rendered + assert "LONG BINARY" in rendered + assert "UNIQUEIDENTIFIER" in rendered diff --git a/tests/unit/dialects/test_sybase.py b/tests/unit/dialects/test_sybase.py new file mode 100644 index 000000000..eb21eeac3 --- /dev/null +++ b/tests/unit/dialects/test_sybase.py @@ -0,0 +1,129 @@ +"""Unit tests for the custom SAP Adaptive Server Enterprise (Sybase ASE) sqlglot dialect.""" + +import sqlglot +from sqlglot import exp, parse_one, transpile +from sqlglot.dialects.dialect import NormalizationStrategy + +from sqlspec.dialects.sybase import Sybase, SybaseTokenizer + +__all__ = () + + +def test_sybase_dialect_registration() -> None: + """Verify Sybase dialect is registered and resolvable by name.""" + dialect = sqlglot.Dialect.get_or_raise("sybase") + assert dialect.__class__ is Sybase + assert dialect.tokenizer_class is SybaseTokenizer + assert dialect.NORMALIZATION_STRATEGY == NormalizationStrategy.CASE_SENSITIVE + + +def test_sybase_top_limit_without_offset() -> None: + """Verify literal integer LIMIT without OFFSET renders unparenthesized SELECT TOP n.""" + result = transpile("SELECT id, name FROM users LIMIT 10", read="postgres", write="sybase")[0] + assert result == "SELECT TOP 10 id, name FROM users" + + +def test_sybase_rows_limit_offset_rendering() -> None: + """Verify LIMIT with OFFSET renders SAP ASE 16.0 SP03+ ROWS LIMIT n OFFSET m.""" + result = transpile("SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 20", write="sybase")[0] + assert result == "SELECT id, name FROM users ORDER BY id ROWS LIMIT 10 OFFSET 20" + + +def test_sybase_rows_offset_only_rendering() -> None: + """Verify OFFSET without LIMIT renders ROWS OFFSET m.""" + result = transpile("SELECT id FROM users ORDER BY id OFFSET 15", write="sybase")[0] + assert result == "SELECT id FROM users ORDER BY id ROWS OFFSET 15" + + +def test_sybase_parameterized_limit_renders_rows_limit() -> None: + """Verify non-literal LIMIT (placeholder/parameter) renders ROWS LIMIT instead of TOP.""" + expr = exp.select("id").from_("users").limit(exp.Placeholder(this="limit")) + rendered = expr.sql(dialect="sybase") + assert rendered == "SELECT id FROM users ROWS LIMIT :limit" + + +def test_sybase_parse_rows_limit_offset_roundtrip() -> None: + """Verify SAP ASE ROWS LIMIT n OFFSET m parses into canonical Limit/Offset AST nodes.""" + parsed = parse_one("SELECT id, name FROM users ORDER BY id ROWS LIMIT 10 OFFSET 20", dialect="sybase") + assert isinstance(parsed, exp.Select) + limit_node = parsed.args.get("limit") + offset_node = parsed.args.get("offset") + assert isinstance(limit_node, exp.Limit) + assert isinstance(offset_node, exp.Offset) + assert parsed.sql(dialect="sybase") == "SELECT id, name FROM users ORDER BY id ROWS LIMIT 10 OFFSET 20" + + +def test_sybase_copy_false_preserves_ast_pagination() -> None: + """Verify expression.sql('sybase', copy=False) never mutates limit or offset in place.""" + select_limit = parse_one("SELECT id FROM users LIMIT 5", read="postgres") + first_limit_sql = select_limit.sql(dialect="sybase", copy=False) + second_limit_sql = select_limit.sql(dialect="sybase", copy=False) + assert first_limit_sql == "SELECT TOP 5 id FROM users" + assert second_limit_sql == first_limit_sql + assert isinstance(select_limit.args.get("limit"), exp.Limit) + + select_page = parse_one("SELECT id FROM users ORDER BY id LIMIT 5 OFFSET 10") + first_page_sql = select_page.sql(dialect="sybase", copy=False) + second_page_sql = select_page.sql(dialect="sybase", copy=False) + assert first_page_sql == "SELECT id FROM users ORDER BY id ROWS LIMIT 5 OFFSET 10" + assert second_page_sql == first_page_sql + assert isinstance(select_page.args.get("limit"), exp.Limit) + assert isinstance(select_page.args.get("offset"), exp.Offset) + + +def test_sybase_set_operation_pagination() -> None: + """Verify UNION pagination renders ROWS LIMIT n OFFSET m and preserves AST under copy=False.""" + union_expr = parse_one("SELECT 1 AS a UNION ALL SELECT 2 AS a LIMIT 5 OFFSET 2") + first_sql = union_expr.sql(dialect="sybase", copy=False) + second_sql = union_expr.sql(dialect="sybase", copy=False) + assert first_sql == "SELECT 1 AS a UNION ALL SELECT 2 AS a ROWS LIMIT 5 OFFSET 2" + assert second_sql == first_sql + + +def test_sybase_type_mappings() -> None: + """Verify Sybase ASE type mappings for TEXT, IMAGE, BIGDATETIME, BIGTIME, UUID, and VARCHAR.""" + assert parse_one("CAST(x AS TEXT)", read="postgres").sql("sybase") == "CAST(x AS TEXT)" + assert parse_one("CAST(x AS JSONB)", read="postgres").sql("sybase") == "CAST(x AS TEXT)" + assert parse_one("CAST(x AS BLOB)", dialect="sybase").sql("sybase") == "CAST(x AS IMAGE)" + assert parse_one("CAST(x AS BYTEA)", read="postgres").sql("sybase") == "CAST(x AS VARBINARY(16384))" + assert parse_one("CAST(x AS TIMESTAMPTZ)", read="postgres").sql("sybase") == "CAST(x AS BIGDATETIME)" + assert parse_one("CAST(x AS TIMESTAMP)", read="postgres").sql("sybase") == "CAST(x AS BIGDATETIME)" + assert parse_one("CAST(x AS TIME)", read="postgres").sql("sybase") == "CAST(x AS BIGTIME)" + assert parse_one("CAST(x AS UUID)", read="postgres").sql("sybase") == "CAST(x AS VARCHAR(36))" + assert parse_one("CAST(x AS VARCHAR)", read="postgres").sql("sybase") == "CAST(x AS VARCHAR(16384))" + assert parse_one("CAST(x AS VARCHAR(MAX))", read="tsql").sql("sybase") == "CAST(x AS TEXT)" + assert parse_one("CAST(x AS NVARCHAR(MAX))", read="tsql").sql("sybase") == "CAST(x AS UNITEXT)" + assert parse_one("CAST(x AS VARBINARY(MAX))", read="tsql").sql("sybase") == "CAST(x AS IMAGE)" + + +def test_sybase_columndef_identity_and_nullable_boolean() -> None: + """Verify Sybase ASE renders bare IDENTITY and converts nullable BIT/BOOLEAN to TINYINT NULL.""" + ddl = parse_one( + "CREATE TABLE items (id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, active BOOLEAN NOT NULL, flag BOOLEAN NULL)", + read="postgres", + ).sql("sybase") + assert "id INT IDENTITY PRIMARY KEY" in ddl + assert "IDENTITY(1, 1)" not in ddl + assert "active BIT NOT NULL" in ddl + assert "flag TINYINT NULL" in ddl + + +def test_sybase_functions_and_operators() -> None: + """Verify CHAR_LENGTH, CHARINDEX, GETUTCDATE, and ILIKE rendering and parsing in Sybase.""" + assert transpile("SELECT LENGTH(name) FROM users", read="postgres", write="sybase")[0] == ( + "SELECT CHAR_LENGTH(name) FROM users" + ) + assert transpile("SELECT STRPOS(name, 'abc') FROM users", read="postgres", write="sybase")[0] == ( + "SELECT CHARINDEX('abc', name) FROM users" + ) + assert transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sybase")[0] == "SELECT GETUTCDATE()" + assert transpile("SELECT * FROM users WHERE name ILIKE '%alice%'", read="postgres", write="sybase")[0] == ( + "SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%alice%')" + ) + + parsed_len = parse_one("SELECT CHAR_LENGTH(name) FROM users", dialect="sybase") + assert parsed_len.find(exp.Length) is not None + parsed_pos = parse_one("SELECT CHARINDEX('abc', name) FROM users", dialect="sybase") + pos_node = parsed_pos.find(exp.StrPosition) + assert pos_node is not None + assert pos_node.sql(dialect="sybase") == "CHARINDEX('abc', name)" From e1da0373a3c0a7ade9770d37f3d0ac6b971bf4fe Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 17:46:37 +0000 Subject: [PATCH 25/35] feat(data-dictionary): add sybase and sqlanywhere catalog queries and fixture identity resync --- sqlspec/builder/_parsing_utils.py | 11 +- sqlspec/data_dictionary/_registry.py | 2 + sqlspec/data_dictionary/_types.py | 6 + sqlspec/data_dictionary/dialects/__init__.py | 4 + .../dialects/sqlanywhere/__init__.py | 31 +++ .../dialects/sqlanywhere/config.py | 191 ++++++++++++++++++ .../dialects/sqlanywhere/sql/columns.sql | 81 ++++++++ .../dialects/sqlanywhere/sql/constraints.sql | 43 ++++ .../dialects/sqlanywhere/sql/foreign_keys.sql | 45 +++++ .../dialects/sqlanywhere/sql/indexes.sql | 39 ++++ .../dialects/sqlanywhere/sql/objects.sql | 18 ++ .../dialects/sqlanywhere/sql/schemas.sql | 15 ++ .../dialects/sqlanywhere/sql/tables.sql | 61 ++++++ .../dialects/sqlanywhere/sql/version.sql | 3 + .../dialects/sqlanywhere/sql/views.sql | 15 ++ .../dialects/sybase/__init__.py | 31 +++ .../data_dictionary/dialects/sybase/config.py | 187 +++++++++++++++++ .../dialects/sybase/sql/columns.sql | 105 ++++++++++ .../dialects/sybase/sql/constraints.sql | 47 +++++ .../dialects/sybase/sql/foreign_keys.sql | 43 ++++ .../dialects/sybase/sql/indexes.sql | 37 ++++ .../dialects/sybase/sql/objects.sql | 21 ++ .../dialects/sybase/sql/schemas.sql | 16 ++ .../dialects/sybase/sql/tables.sql | 62 ++++++ .../dialects/sybase/sql/version.sql | 3 + .../dialects/sybase/sql/views.sql | 15 ++ sqlspec/utils/fixtures.py | 78 ++++++- .../test_sybase_sqlanywhere_dialects.py | 162 +++++++++++++++ tests/unit/utils/test_fixtures_sybase.py | 167 +++++++++++++++ 29 files changed, 1527 insertions(+), 12 deletions(-) create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/__init__.py create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/config.py create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/constraints.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/foreign_keys.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/indexes.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/objects.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/schemas.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/version.sql create mode 100644 sqlspec/data_dictionary/dialects/sqlanywhere/sql/views.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/__init__.py create mode 100644 sqlspec/data_dictionary/dialects/sybase/config.py create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/columns.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/objects.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/schemas.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/tables.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/version.sql create mode 100644 sqlspec/data_dictionary/dialects/sybase/sql/views.sql create mode 100644 tests/unit/data_dictionary/test_sybase_sqlanywhere_dialects.py create mode 100644 tests/unit/utils/test_fixtures_sybase.py diff --git a/sqlspec/builder/_parsing_utils.py b/sqlspec/builder/_parsing_utils.py index 5dc007d40..c53c9af6a 100644 --- a/sqlspec/builder/_parsing_utils.py +++ b/sqlspec/builder/_parsing_utils.py @@ -146,18 +146,19 @@ def parse_table_expression( BigQuery's ``\`project.dataset.table\``` are parsed into their qualified parts instead of a single literal name. """ + resolved_dialect: DialectType | None = _normalize_dialect(dialect) if isinstance(dialect, str) else dialect if explicit_alias is None and " " in table_input.strip(): parts = table_input.strip().split(None, 1) if len(parts) == ALIAS_PARTS_EXPECTED_COUNT: base_table, alias = parts if _is_simple_identifier(base_table): - return exp.to_table(base_table, alias=alias, dialect=dialect) + return exp.to_table(base_table, alias=alias, dialect=resolved_dialect) if _is_simple_identifier(table_input): - return exp.to_table(table_input, alias=explicit_alias, dialect=dialect) + return exp.to_table(table_input, alias=explicit_alias, dialect=resolved_dialect) with contextlib.suppress(Exception): - parsed: exp.Expr | None = exp.maybe_parse(f"SELECT * FROM {table_input}", dialect=dialect) + parsed: exp.Expr | None = exp.maybe_parse(f"SELECT * FROM {table_input}", dialect=resolved_dialect) if isinstance(parsed, exp.Select): from_clause = parsed.find(exp.From) if from_clause is not None: @@ -168,7 +169,9 @@ def parse_table_expression( return exp.alias_(table_expr, explicit_alias) return table_expr # type: ignore[no-any-return] - return _mark_explicit_table_quotes(exp.to_table(table_input, alias=explicit_alias, dialect=dialect), table_input) + return _mark_explicit_table_quotes( + exp.to_table(table_input, alias=explicit_alias, dialect=resolved_dialect), table_input + ) def parse_order_expression(order_input: str | exp.Expr) -> exp.Expr: diff --git a/sqlspec/data_dictionary/_registry.py b/sqlspec/data_dictionary/_registry.py index 74ba43e60..f5ee61c04 100644 --- a/sqlspec/data_dictionary/_registry.py +++ b/sqlspec/data_dictionary/_registry.py @@ -24,6 +24,8 @@ "cockroach": "cockroachdb", "tsql": "mssql", "sqlserver": "mssql", + "ase": "sybase", + "sa": "sqlanywhere", } DIALECT_MODE_ALIASES: dict[tuple[str, str], str] = { diff --git a/sqlspec/data_dictionary/_types.py b/sqlspec/data_dictionary/_types.py index fa022e5b7..57eabc489 100644 --- a/sqlspec/data_dictionary/_types.py +++ b/sqlspec/data_dictionary/_types.py @@ -1470,6 +1470,8 @@ class FeatureFlags(TypedDict, total=False): """Whether native JSON or JSONB column types and operators are supported.""" supports_maps: bool """Whether native key-value ``MAP`` column types are supported.""" + supports_merge: bool + """Whether SQL MERGE statements are supported.""" supports_on_conflict: bool """Whether ``INSERT ... ON CONFLICT`` conflict resolution syntax is supported.""" supports_partitioning: bool @@ -1492,6 +1494,8 @@ class FeatureFlags(TypedDict, total=False): """Whether ``FOR UPDATE SKIP LOCKED`` non-blocking queue scans are supported.""" supports_structs: bool """Whether composite ``STRUCT`` / ``ROW`` column types are supported.""" + supports_transactional_ddl: bool + """Whether schema DDL statements can be executed within a transaction.""" supports_transactions: bool """Whether multi-statement ACID transactions are supported.""" supports_update_from: bool @@ -1525,6 +1529,8 @@ class FeatureVersions(TypedDict, total=False): """Minimum engine version supporting native JSON types and functions.""" supports_jsonb: "VersionInfo" """Minimum engine version supporting binary JSONB storage.""" + supports_merge: "VersionInfo" + """Minimum engine version supporting SQL MERGE statements.""" supports_partitioning: "VersionInfo" """Minimum engine version supporting declarative table partitioning.""" supports_resource_groups: "VersionInfo" diff --git a/sqlspec/data_dictionary/dialects/__init__.py b/sqlspec/data_dictionary/dialects/__init__.py index 2117887c8..86f3135e6 100644 --- a/sqlspec/data_dictionary/dialects/__init__.py +++ b/sqlspec/data_dictionary/dialects/__init__.py @@ -9,7 +9,9 @@ from sqlspec.data_dictionary.dialects.oracle import ORACLE_CONFIG from sqlspec.data_dictionary.dialects.postgres import POSTGRES_CONFIG from sqlspec.data_dictionary.dialects.spanner import SPANNER_CONFIG +from sqlspec.data_dictionary.dialects.sqlanywhere import SQLANYWHERE_CONFIG from sqlspec.data_dictionary.dialects.sqlite import SQLITE_CONFIG +from sqlspec.data_dictionary.dialects.sybase import SYBASE_CONFIG __all__ = ( "BIGQUERY_CONFIG", @@ -22,5 +24,7 @@ "ORACLE_CONFIG", "POSTGRES_CONFIG", "SPANNER_CONFIG", + "SQLANYWHERE_CONFIG", "SQLITE_CONFIG", + "SYBASE_CONFIG", ) diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/__init__.py b/sqlspec/data_dictionary/dialects/sqlanywhere/__init__.py new file mode 100644 index 000000000..3356a932c --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/__init__.py @@ -0,0 +1,31 @@ +"""SAP SQL Anywhere dialect static configuration and reflection helpers.""" + +from sqlspec.data_dictionary.dialects.sqlanywhere.config import ( + SQLANYWHERE_CONFIG, + SQLANYWHERE_FEATURE_FLAGS, + SQLANYWHERE_FEATURE_VERSIONS, + SQLANYWHERE_METADATA_DOMAINS, + SQLANYWHERE_TYPE_MAPPINGS, + SQLANYWHERE_VERSION_PATTERN, + SqlanywhereVersionInfo, + build_sqlanywhere_metadata_capability_profile, + extract_sqlanywhere_version_value, + list_sqlanywhere_available_features, + merge_sqlanywhere_table_lists, + resolve_sqlanywhere_feature_flag, +) + +__all__ = ( + "SQLANYWHERE_CONFIG", + "SQLANYWHERE_FEATURE_FLAGS", + "SQLANYWHERE_FEATURE_VERSIONS", + "SQLANYWHERE_METADATA_DOMAINS", + "SQLANYWHERE_TYPE_MAPPINGS", + "SQLANYWHERE_VERSION_PATTERN", + "SqlanywhereVersionInfo", + "build_sqlanywhere_metadata_capability_profile", + "extract_sqlanywhere_version_value", + "list_sqlanywhere_available_features", + "merge_sqlanywhere_table_lists", + "resolve_sqlanywhere_feature_flag", +) diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/config.py b/sqlspec/data_dictionary/dialects/sqlanywhere/config.py new file mode 100644 index 000000000..51246d50a --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/config.py @@ -0,0 +1,191 @@ +"""SAP SQL Anywhere dialect static configuration and reflection helpers.""" + +import re +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Final + +from sqlspec.data_dictionary import register_dialect +from sqlspec.data_dictionary._types import ( + DialectConfig, + FeatureFlags, + FeatureVersions, + MetadataCapability, + MetadataCapabilityProfile, + MetadataFidelity, + MetadataSource, + MetadataSupport, + VersionInfo, +) + +if TYPE_CHECKING: + from sqlspec.data_dictionary import TableMetadata + +__all__ = ( + "SQLANYWHERE_CONFIG", + "SQLANYWHERE_FEATURE_FLAGS", + "SQLANYWHERE_FEATURE_VERSIONS", + "SQLANYWHERE_METADATA_DOMAINS", + "SQLANYWHERE_TYPE_MAPPINGS", + "SQLANYWHERE_VERSION_PATTERN", + "SqlanywhereVersionInfo", + "build_sqlanywhere_metadata_capability_profile", + "extract_sqlanywhere_version_value", + "list_sqlanywhere_available_features", + "merge_sqlanywhere_table_lists", + "resolve_sqlanywhere_feature_flag", +) + +SQLANYWHERE_VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)") + +SQLANYWHERE_FEATURE_VERSIONS: FeatureVersions = { + "supports_cte": VersionInfo(10, 0, 0), + "supports_window_functions": VersionInfo(10, 0, 0), + "supports_merge": VersionInfo(11, 0, 0), +} + +SQLANYWHERE_FEATURE_FLAGS: FeatureFlags = { + "supports_clustering": False, + "supports_returning": False, + "supports_upsert": True, + "supports_on_conflict": False, + "supports_window_functions": True, + "supports_cte": True, + "supports_json": False, + "supports_arrays": False, + "supports_uuid": True, + "supports_schemas": True, + "supports_merge": True, + "supports_for_update": True, + "supports_skip_locked": True, + "supports_transactional_ddl": True, + "supports_transactions": True, + "supports_prepared_statements": True, + "supports_in_memory": False, + "supports_update_from": True, + "supports_sequences": True, +} + +SQLANYWHERE_TYPE_MAPPINGS: dict[str, str] = { + "uuid": "UNIQUEIDENTIFIER", + "boolean": "BIT", + "timestamp": "TIMESTAMP", + "datetime": "TIMESTAMP", + "date": "DATE", + "time": "TIME", + "text": "LONG VARCHAR", + "blob": "LONG BINARY", + "bytes": "VARBINARY(32767)", + "json": "LONG VARCHAR", + "decimal": "DECIMAL", + "float": "DOUBLE", + "integer": "INTEGER", + "bigint": "BIGINT", + "smallint": "SMALLINT", + "string": "VARCHAR(255)", + "varchar": "VARCHAR(255)", + "AutoIncrement": "INTEGER DEFAULT AUTOINCREMENT", +} + +SQLANYWHERE_CONFIG = DialectConfig( + name="sqlanywhere", + feature_versions=SQLANYWHERE_FEATURE_VERSIONS, + feature_flags=SQLANYWHERE_FEATURE_FLAGS, + type_mappings=SQLANYWHERE_TYPE_MAPPINGS, + version_pattern=SQLANYWHERE_VERSION_PATTERN, + default_schema="DBA", + parameter_style="qmark", +) + +register_dialect(SQLANYWHERE_CONFIG) + +SQLANYWHERE_METADATA_DOMAINS: Final[tuple[str, ...]] = ( + "schemas", + "objects", + "tables", + "columns", + "constraints", + "indexes", + "foreign_keys", + "views", + "version", +) + + +class SqlanywhereVersionInfo(VersionInfo): + """SAP SQL Anywhere database version info with optional build number.""" + + __slots__ = ("build_number",) + + def __init__(self, major: int, minor: int = 0, patch: int = 0, build_number: str | None = None) -> None: + """Initialize SAP SQL Anywhere version info.""" + super().__init__(major, minor, patch) + self.build_number = build_number + + def __str__(self) -> str: + """Format version string including build number when present.""" + base = f"{self.major}.{self.minor}.{self.patch}" + return f"{base} ({self.build_number})" if self.build_number else base + + +def extract_sqlanywhere_version_value(row: object) -> str | None: + """Extract a SAP SQL Anywhere version string from a row-like object.""" + if isinstance(row, Mapping): + for key in ("version_string", "VERSION_STRING", "version", "VERSION"): + value = row.get(key) + if value: + return str(value) + if isinstance(row, (list, tuple)) and row: + return str(row[0]) + if row is not None: + return str(row) + return None + + +def resolve_sqlanywhere_feature_flag(feature: str, version_info: VersionInfo | None) -> bool: + """Resolve a SAP SQL Anywhere feature flag using config and version details.""" + flag = SQLANYWHERE_CONFIG.get_feature_flag(feature) + if flag is not None: + return bool(flag) + required_version = SQLANYWHERE_CONFIG.get_feature_version(feature) + if required_version is None or version_info is None: + return False + return bool(version_info >= required_version) + + +def list_sqlanywhere_available_features() -> list[str]: + """List static and versioned SAP SQL Anywhere data-dictionary feature flags.""" + features = set(SQLANYWHERE_CONFIG.feature_flags.keys()) + features.update(SQLANYWHERE_CONFIG.feature_versions.keys()) + return sorted(features) + + +def merge_sqlanywhere_table_lists( + ordered: list["TableMetadata"], all_tables: list["TableMetadata"] +) -> list["TableMetadata"]: + """Merge dependency-ordered SAP SQL Anywhere tables with catalog remainder rows.""" + if not ordered: + return sorted(all_tables, key=lambda item: item.get("table_name") or "") + seen = {(item.get("schema_name"), item.get("table_name")) for item in ordered if item.get("table_name")} + remainder = [item for item in all_tables if (item.get("schema_name"), item.get("table_name")) not in seen] + return ordered + remainder + + +def build_sqlanywhere_metadata_capability_profile( + adapter: str | None, domains: Sequence[str] | None = None +) -> MetadataCapabilityProfile: + """Build SAP SQL Anywhere data-dictionary capability metadata.""" + requested_domains = tuple(domains) if domains is not None else SQLANYWHERE_METADATA_DOMAINS + capabilities: list[MetadataCapability] = [] + for domain in requested_domains: + if domain in SQLANYWHERE_METADATA_DOMAINS: + capabilities.append( + MetadataCapability( + domain=domain, + support=MetadataSupport.SUPPORTED, + fidelity=MetadataFidelity.NATIVE, + source=MetadataSource.CATALOG, + ) + ) + else: + capabilities.append(MetadataCapability.unsupported(domain)) + return MetadataCapabilityProfile("sqlanywhere", adapter=adapter, capabilities=tuple(capabilities)) diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql new file mode 100644 index 000000000..09be767eb --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql @@ -0,0 +1,81 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + c.column_name AS column_name, + d.domain_name AS data_type, + CASE c.nulls WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable, + c."default" AS column_default, + c.column_id AS ordinal_position, + c.width AS char_length, + c.width AS max_length, + c.width AS numeric_precision, + c.scale AS numeric_scale, + CASE WHEN c.pkey = 'Y' THEN 1 ELSE 0 END AS is_primary, + CASE + WHEN EXISTS ( + SELECT 1 + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i.table_id = t.table_id + AND i."unique" IN (1, 2) + AND ic.column_id = c.column_id + ) THEN 1 + ELSE 0 + END AS is_unique, + CASE + WHEN c."default" LIKE '%autoincrement%' THEN 'AUTOINCREMENT' + ELSE NULL + END AS identity_generation, + CASE WHEN c.column_type = 'C' THEN 1 ELSE 0 END AS is_generated, + NULL AS column_comment +FROM SYS.SYSTABCOL c +JOIN SYS.SYSTAB t ON c.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +JOIN SYS.SYSDOMAIN d ON c.domain_id = d.domain_id +WHERE t.table_type IN (1, 21) + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) +ORDER BY u.user_name, t.table_name, c.column_id; + +-- name: by_table +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + c.column_name AS column_name, + d.domain_name AS data_type, + CASE c.nulls WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable, + c."default" AS column_default, + c.column_id AS ordinal_position, + c.width AS char_length, + c.width AS max_length, + c.width AS numeric_precision, + c.scale AS numeric_scale, + CASE WHEN c.pkey = 'Y' THEN 1 ELSE 0 END AS is_primary, + CASE + WHEN EXISTS ( + SELECT 1 + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i.table_id = t.table_id + AND i."unique" IN (1, 2) + AND ic.column_id = c.column_id + ) THEN 1 + ELSE 0 + END AS is_unique, + CASE + WHEN c."default" LIKE '%autoincrement%' THEN 'AUTOINCREMENT' + ELSE NULL + END AS identity_generation, + CASE WHEN c.column_type = 'C' THEN 1 ELSE 0 END AS is_generated, + NULL AS column_comment +FROM SYS.SYSTABCOL c +JOIN SYS.SYSTAB t ON c.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +JOIN SYS.SYSDOMAIN d ON c.domain_id = d.domain_id +WHERE t.table_type IN (1, 21) + AND t.table_name = :table_name + AND u.user_name = COALESCE(:schema_name, CURRENT USER) +ORDER BY c.column_id; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/constraints.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/constraints.sql new file mode 100644 index 000000000..f33deff1c --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/constraints.sql @@ -0,0 +1,43 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + i.index_name AS constraint_name, + CASE i.index_category + WHEN 1 THEN 'PRIMARY KEY' + WHEN 2 THEN 'FOREIGN KEY' + ELSE 'UNIQUE' + END AS constraint_type, + NULL AS check_clause, + 'Y' AS is_enforced +FROM SYS.SYSIDX i +JOIN SYS.SYSTAB t ON i.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type = 1 + AND (i.index_category IN (1, 2) OR i."unique" IN (1, 2)) + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) +ORDER BY u.user_name, t.table_name, i.index_name; + +-- name: by_table +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + i.index_name AS constraint_name, + CASE i.index_category + WHEN 1 THEN 'PRIMARY KEY' + WHEN 2 THEN 'FOREIGN KEY' + ELSE 'UNIQUE' + END AS constraint_type, + NULL AS check_clause, + 'Y' AS is_enforced +FROM SYS.SYSIDX i +JOIN SYS.SYSTAB t ON i.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type = 1 + AND (i.index_category IN (1, 2) OR i."unique" IN (1, 2)) + AND t.table_name = :table_name + AND u.user_name = COALESCE(:schema_name, CURRENT USER) +ORDER BY i.index_name; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/foreign_keys.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/foreign_keys.sql new file mode 100644 index 000000000..22d98b4a7 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/foreign_keys.sql @@ -0,0 +1,45 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + fu.user_name AS schema_name, + ft.table_name AS table_name, + fi.index_name AS constraint_name, + fc.column_name AS column_name, + pu.user_name AS referenced_schema, + pt.table_name AS referenced_table, + pc.column_name AS referenced_column +FROM SYS.SYSFKEY fk +JOIN SYS.SYSTAB ft ON fk.foreign_table_id = ft.table_id +JOIN SYS.SYSUSER fu ON ft.creator = fu.user_id +JOIN SYS.SYSIDX fi ON fk.foreign_table_id = fi.table_id AND fk.foreign_index_id = fi.index_id +JOIN SYS.SYSIDXCOL fic ON fi.table_id = fic.table_id AND fi.index_id = fic.index_id +JOIN SYS.SYSTABCOL fc ON fic.table_id = fc.table_id AND fic.column_id = fc.column_id +JOIN SYS.SYSTAB pt ON fk.primary_table_id = pt.table_id +JOIN SYS.SYSUSER pu ON pt.creator = pu.user_id +JOIN SYS.SYSTABCOL pc ON pt.table_id = pc.table_id AND fic.primary_column_id = pc.column_id +WHERE fu.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR ft.table_name = :table_name) +ORDER BY fu.user_name, ft.table_name, fi.index_name, fic.sequence; + +-- name: by_table +-- dialect: sqlanywhere +SELECT + fu.user_name AS schema_name, + ft.table_name AS table_name, + fi.index_name AS constraint_name, + fc.column_name AS column_name, + pu.user_name AS referenced_schema, + pt.table_name AS referenced_table, + pc.column_name AS referenced_column +FROM SYS.SYSFKEY fk +JOIN SYS.SYSTAB ft ON fk.foreign_table_id = ft.table_id +JOIN SYS.SYSUSER fu ON ft.creator = fu.user_id +JOIN SYS.SYSIDX fi ON fk.foreign_table_id = fi.table_id AND fk.foreign_index_id = fi.index_id +JOIN SYS.SYSIDXCOL fic ON fi.table_id = fic.table_id AND fi.index_id = fic.index_id +JOIN SYS.SYSTABCOL fc ON fic.table_id = fc.table_id AND fic.column_id = fc.column_id +JOIN SYS.SYSTAB pt ON fk.primary_table_id = pt.table_id +JOIN SYS.SYSUSER pu ON pt.creator = pu.user_id +JOIN SYS.SYSTABCOL pc ON pt.table_id = pc.table_id AND fic.primary_column_id = pc.column_id +WHERE ft.table_name = :table_name + AND fu.user_name = COALESCE(:schema_name, CURRENT USER) +ORDER BY fi.index_name, fic.sequence; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/indexes.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/indexes.sql new file mode 100644 index 000000000..bfa60c3cb --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/indexes.sql @@ -0,0 +1,39 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + i.index_name AS index_name, + c.column_name AS column_name, + ic.sequence + 1 AS column_position, + CASE WHEN i."unique" IN (1, 2) THEN 1 ELSE 0 END AS is_unique, + CASE WHEN i.index_category = 1 THEN 1 ELSE 0 END AS is_primary +FROM SYS.SYSIDX i +JOIN SYS.SYSTAB t ON i.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id +JOIN SYS.SYSTABCOL c ON ic.table_id = c.table_id AND ic.column_id = c.column_id +WHERE t.table_type = 1 + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) +ORDER BY u.user_name, t.table_name, i.index_name, ic.sequence; + +-- name: by_table +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + i.index_name AS index_name, + c.column_name AS column_name, + ic.sequence + 1 AS column_position, + CASE WHEN i."unique" IN (1, 2) THEN 1 ELSE 0 END AS is_unique, + CASE WHEN i.index_category = 1 THEN 1 ELSE 0 END AS is_primary +FROM SYS.SYSIDX i +JOIN SYS.SYSTAB t ON i.table_id = t.table_id +JOIN SYS.SYSUSER u ON t.creator = u.user_id +JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id +JOIN SYS.SYSTABCOL c ON ic.table_id = c.table_id AND ic.column_id = c.column_id +WHERE t.table_type = 1 + AND t.table_name = :table_name + AND u.user_name = COALESCE(:schema_name, CURRENT USER) +ORDER BY i.index_name, ic.sequence; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/objects.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/objects.sql new file mode 100644 index 000000000..eba975706 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/objects.sql @@ -0,0 +1,18 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS object_name, + CASE t.table_type + WHEN 1 THEN 'TABLE' + WHEN 21 THEN 'VIEW' + ELSE 'OBJECT' + END AS object_type, + NULL AS created, + NULL AS remarks +FROM SYS.SYSTAB t +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type IN (1, 21) + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:object_name IS NULL OR t.table_name = :object_name) +ORDER BY u.user_name, object_type, t.table_name; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/schemas.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/schemas.sql new file mode 100644 index 000000000..8feff6bbc --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/schemas.sql @@ -0,0 +1,15 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + u.user_name AS owner_name, + NULL AS create_time, + NULL AS comment +FROM SYS.SYSUSER u +WHERE u.user_name NOT IN ('SYS', 'PUBLIC', 'dbo', 'diagnostics', 'rs_systabgroup', 'SA_DEBUG', 'SYS_SPATIAL_ADMIN_ROLE') + AND (:schema_name IS NULL OR u.user_name = :schema_name) +ORDER BY u.user_name; + +-- name: current +-- dialect: sqlanywhere +SELECT CURRENT USER AS schema_name; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql new file mode 100644 index 000000000..993eda662 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql @@ -0,0 +1,61 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + 'BASE TABLE' AS table_type, + CASE + WHEN EXISTS ( + SELECT 1 + FROM SYS.SYSFKEY fk + WHERE fk.foreign_table_id = t.table_id + AND fk.primary_table_id <> t.table_id + ) THEN 1 + ELSE 0 + END AS dependency_level, + CASE + WHEN EXISTS ( + SELECT 1 + FROM SYS.SYSFKEY fk + WHERE fk.foreign_table_id = t.table_id + AND fk.primary_table_id <> t.table_id + ) THEN 1 + ELSE 0 + END AS level +FROM SYS.SYSTAB t +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type = 1 + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) +ORDER BY dependency_level, u.user_name, t.table_name; + +-- name: all_by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS table_name, + CASE t.table_type + WHEN 1 THEN 'BASE TABLE' + WHEN 21 THEN 'VIEW' + ELSE 'TABLE' + END AS table_type, + 0 AS dependency_level, + 0 AS level, + t.count AS estimated_row_count, + NULL AS table_comment +FROM SYS.SYSTAB t +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type IN (1, 21) + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) +ORDER BY u.user_name, t.table_name; + +-- name: names_by_schema +-- dialect: sqlanywhere +SELECT + t.table_name AS table_name +FROM SYS.SYSTAB t +JOIN SYS.SYSUSER u ON t.creator = u.user_id +WHERE t.table_type = 1 + AND u.user_name = COALESCE(:schema_name, CURRENT USER) +ORDER BY t.table_name; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/version.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/version.sql new file mode 100644 index 000000000..225d43d69 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/version.sql @@ -0,0 +1,3 @@ +-- name: current +-- dialect: sqlanywhere +SELECT PROPERTY('ProductVersion') AS version_string; diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/views.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/views.sql new file mode 100644 index 000000000..5ba790188 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/views.sql @@ -0,0 +1,15 @@ +-- name: by_schema +-- dialect: sqlanywhere +SELECT + u.user_name AS schema_name, + t.table_name AS view_name, + v.view_def AS definition, + 0 AS is_read_only, + 1 AS is_valid +FROM SYS.SYSTAB t +JOIN SYS.SYSUSER u ON t.creator = u.user_id +LEFT JOIN SYS.SYSVIEW v ON t.object_id = v.view_object_id +WHERE t.table_type = 21 + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:view_name IS NULL OR t.table_name = :view_name) +ORDER BY u.user_name, t.table_name; diff --git a/sqlspec/data_dictionary/dialects/sybase/__init__.py b/sqlspec/data_dictionary/dialects/sybase/__init__.py new file mode 100644 index 000000000..57369bf58 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/__init__.py @@ -0,0 +1,31 @@ +"""Sybase ASE dialect static configuration and reflection helpers.""" + +from sqlspec.data_dictionary.dialects.sybase.config import ( + SYBASE_CONFIG, + SYBASE_FEATURE_FLAGS, + SYBASE_FEATURE_VERSIONS, + SYBASE_METADATA_DOMAINS, + SYBASE_TYPE_MAPPINGS, + SYBASE_VERSION_PATTERN, + SybaseVersionInfo, + build_sybase_metadata_capability_profile, + extract_sybase_version_value, + list_sybase_available_features, + merge_sybase_table_lists, + resolve_sybase_feature_flag, +) + +__all__ = ( + "SYBASE_CONFIG", + "SYBASE_FEATURE_FLAGS", + "SYBASE_FEATURE_VERSIONS", + "SYBASE_METADATA_DOMAINS", + "SYBASE_TYPE_MAPPINGS", + "SYBASE_VERSION_PATTERN", + "SybaseVersionInfo", + "build_sybase_metadata_capability_profile", + "extract_sybase_version_value", + "list_sybase_available_features", + "merge_sybase_table_lists", + "resolve_sybase_feature_flag", +) diff --git a/sqlspec/data_dictionary/dialects/sybase/config.py b/sqlspec/data_dictionary/dialects/sybase/config.py new file mode 100644 index 000000000..e4df1d757 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/config.py @@ -0,0 +1,187 @@ +"""Sybase ASE dialect static configuration and reflection helpers.""" + +import re +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Final + +from sqlspec.data_dictionary import register_dialect +from sqlspec.data_dictionary._types import ( + DialectConfig, + FeatureFlags, + FeatureVersions, + MetadataCapability, + MetadataCapabilityProfile, + MetadataFidelity, + MetadataSource, + MetadataSupport, + VersionInfo, +) + +if TYPE_CHECKING: + from sqlspec.data_dictionary import TableMetadata + +__all__ = ( + "SYBASE_CONFIG", + "SYBASE_FEATURE_FLAGS", + "SYBASE_FEATURE_VERSIONS", + "SYBASE_METADATA_DOMAINS", + "SYBASE_TYPE_MAPPINGS", + "SYBASE_VERSION_PATTERN", + "SybaseVersionInfo", + "build_sybase_metadata_capability_profile", + "extract_sybase_version_value", + "list_sybase_available_features", + "merge_sybase_table_lists", + "resolve_sybase_feature_flag", +) + +SYBASE_VERSION_PATTERN = re.compile(r"Adaptive Server Enterprise/(\d+)\.(\d+)(?:\s+SP(\d+)|\.\d+)?", re.IGNORECASE) + +SYBASE_FEATURE_VERSIONS: FeatureVersions = {} + +SYBASE_FEATURE_FLAGS: FeatureFlags = { + "supports_clustering": False, + "supports_returning": False, + "supports_upsert": False, + "supports_on_conflict": False, + "supports_window_functions": False, + "supports_cte": False, + "supports_json": False, + "supports_arrays": False, + "supports_uuid": False, + "supports_schemas": True, + "supports_merge": False, + "supports_for_update": False, + "supports_skip_locked": False, + "supports_transactional_ddl": False, + "supports_transactions": True, + "supports_prepared_statements": True, + "supports_in_memory": False, + "supports_update_from": True, + "supports_sequences": False, +} + +SYBASE_TYPE_MAPPINGS: dict[str, str] = { + "uuid": "VARCHAR(36)", + "boolean": "BIT", + "timestamp": "BIGDATETIME", + "datetime": "BIGDATETIME", + "date": "DATE", + "time": "BIGTIME", + "text": "TEXT", + "blob": "IMAGE", + "bytes": "VARBINARY(16384)", + "json": "TEXT", + "decimal": "DECIMAL", + "float": "FLOAT", + "integer": "INT", + "bigint": "BIGINT", + "smallint": "SMALLINT", + "string": "VARCHAR(255)", + "varchar": "VARCHAR(255)", + "AutoIncrement": "INT IDENTITY", +} + +SYBASE_CONFIG = DialectConfig( + name="sybase", + feature_versions=SYBASE_FEATURE_VERSIONS, + feature_flags=SYBASE_FEATURE_FLAGS, + type_mappings=SYBASE_TYPE_MAPPINGS, + version_pattern=SYBASE_VERSION_PATTERN, + default_schema="dbo", + parameter_style="pyformat_named", +) + +register_dialect(SYBASE_CONFIG) + +SYBASE_METADATA_DOMAINS: Final[tuple[str, ...]] = ( + "schemas", + "objects", + "tables", + "columns", + "constraints", + "indexes", + "foreign_keys", + "views", + "version", +) + + +class SybaseVersionInfo(VersionInfo): + """Sybase ASE database version info with optional service pack string.""" + + __slots__ = ("service_pack",) + + def __init__(self, major: int, minor: int = 0, patch: int = 0, service_pack: str | None = None) -> None: + """Initialize Sybase ASE version info.""" + super().__init__(major, minor, patch) + self.service_pack = service_pack + + def __str__(self) -> str: + """Format version string including service pack when present.""" + base = f"{self.major}.{self.minor}.{self.patch}" + return f"{base} ({self.service_pack})" if self.service_pack else base + + +def extract_sybase_version_value(row: object) -> str | None: + """Extract a Sybase ASE version string from a row-like object.""" + if isinstance(row, Mapping): + for key in ("version_string", "VERSION_STRING", "version", "VERSION"): + value = row.get(key) + if value: + return str(value) + if isinstance(row, (list, tuple)) and row: + return str(row[0]) + if row is not None: + return str(row) + return None + + +def resolve_sybase_feature_flag(feature: str, version_info: VersionInfo | None) -> bool: + """Resolve a Sybase ASE feature flag using config and version details.""" + flag = SYBASE_CONFIG.get_feature_flag(feature) + if flag is not None: + return bool(flag) + required_version = SYBASE_CONFIG.get_feature_version(feature) + if required_version is None or version_info is None: + return False + return bool(version_info >= required_version) + + +def list_sybase_available_features() -> list[str]: + """List static and versioned Sybase ASE data-dictionary feature flags.""" + features = set(SYBASE_CONFIG.feature_flags.keys()) + features.update(SYBASE_CONFIG.feature_versions.keys()) + return sorted(features) + + +def merge_sybase_table_lists( + ordered: list["TableMetadata"], all_tables: list["TableMetadata"] +) -> list["TableMetadata"]: + """Merge dependency-ordered Sybase ASE tables with catalog remainder rows.""" + if not ordered: + return sorted(all_tables, key=lambda item: item.get("table_name") or "") + seen = {(item.get("schema_name"), item.get("table_name")) for item in ordered if item.get("table_name")} + remainder = [item for item in all_tables if (item.get("schema_name"), item.get("table_name")) not in seen] + return ordered + remainder + + +def build_sybase_metadata_capability_profile( + adapter: str | None, domains: Sequence[str] | None = None +) -> MetadataCapabilityProfile: + """Build Sybase ASE data-dictionary capability metadata.""" + requested_domains = tuple(domains) if domains is not None else SYBASE_METADATA_DOMAINS + capabilities: list[MetadataCapability] = [] + for domain in requested_domains: + if domain in SYBASE_METADATA_DOMAINS: + capabilities.append( + MetadataCapability( + domain=domain, + support=MetadataSupport.SUPPORTED, + fidelity=MetadataFidelity.NATIVE, + source=MetadataSource.CATALOG, + ) + ) + else: + capabilities.append(MetadataCapability.unsupported(domain)) + return MetadataCapabilityProfile("sybase", adapter=adapter, capabilities=tuple(capabilities)) diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql b/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql new file mode 100644 index 000000000..fead85118 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql @@ -0,0 +1,105 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + c.name AS column_name, + t.name AS data_type, + CASE WHEN (c.status & 8) = 8 THEN 'YES' ELSE 'NO' END AS is_nullable, + cm.text AS column_default, + c.colid AS ordinal_position, + c.length AS char_length, + c.length AS max_length, + c.prec AS numeric_precision, + c.scale AS numeric_scale, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysindexes i + WHERE i.id = o.id + AND (i.status & 2048) = 2048 + AND ( + index_col(u.name + '.' + o.name, i.indid, 1) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 2) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 3) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 4) = c.name + ) + ) THEN 1 + ELSE 0 + END AS is_primary, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysindexes i + WHERE i.id = o.id + AND (i.status & 2) = 2 + AND i.keycnt = 1 + AND index_col(u.name + '.' + o.name, i.indid, 1) = c.name + ) THEN 1 + ELSE 0 + END AS is_unique, + CASE WHEN (c.status & 128) = 128 THEN 'YES' ELSE NULL END AS identity_generation, + 0 AS is_generated, + NULL AS column_comment +FROM dbo.syscolumns c +JOIN dbo.sysobjects o ON c.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.systypes t ON c.usertype = t.usertype +LEFT JOIN dbo.syscomments cm ON c.cdefault = cm.id AND cm.colid = 1 +WHERE o.type IN ('U', 'V') + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY u.name, o.name, c.colid; + +-- name: by_table +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + c.name AS column_name, + t.name AS data_type, + CASE WHEN (c.status & 8) = 8 THEN 'YES' ELSE 'NO' END AS is_nullable, + cm.text AS column_default, + c.colid AS ordinal_position, + c.length AS char_length, + c.length AS max_length, + c.prec AS numeric_precision, + c.scale AS numeric_scale, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysindexes i + WHERE i.id = o.id + AND (i.status & 2048) = 2048 + AND ( + index_col(u.name + '.' + o.name, i.indid, 1) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 2) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 3) = c.name + OR index_col(u.name + '.' + o.name, i.indid, 4) = c.name + ) + ) THEN 1 + ELSE 0 + END AS is_primary, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysindexes i + WHERE i.id = o.id + AND (i.status & 2) = 2 + AND i.keycnt = 1 + AND index_col(u.name + '.' + o.name, i.indid, 1) = c.name + ) THEN 1 + ELSE 0 + END AS is_unique, + CASE WHEN (c.status & 128) = 128 THEN 'YES' ELSE NULL END AS identity_generation, + 0 AS is_generated, + NULL AS column_comment +FROM dbo.syscolumns c +JOIN dbo.sysobjects o ON c.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.systypes t ON c.usertype = t.usertype +LEFT JOIN dbo.syscomments cm ON c.cdefault = cm.id AND cm.colid = 1 +WHERE o.type IN ('U', 'V') + AND o.name = :table_name + AND u.name = COALESCE(:schema_name, USER_NAME()) +ORDER BY c.colid; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql b/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql new file mode 100644 index 000000000..377c96e13 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql @@ -0,0 +1,47 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + co.name AS constraint_name, + CASE + WHEN (c.status & 1) = 1 THEN 'PRIMARY KEY' + WHEN (c.status & 2) = 2 THEN 'UNIQUE' + WHEN (c.status & 64) = 64 THEN 'FOREIGN KEY' + WHEN (c.status & 128) = 128 THEN 'CHECK' + ELSE 'CONSTRAINT' + END AS constraint_type, + cm.text AS check_clause, + 'Y' AS is_enforced +FROM dbo.sysconstraints c +JOIN dbo.sysobjects o ON c.tableid = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.sysobjects co ON c.constrid = co.id +LEFT JOIN dbo.syscomments cm ON co.id = cm.id AND cm.colid = 1 +WHERE u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY u.name, o.name, co.name; + +-- name: by_table +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + co.name AS constraint_name, + CASE + WHEN (c.status & 1) = 1 THEN 'PRIMARY KEY' + WHEN (c.status & 2) = 2 THEN 'UNIQUE' + WHEN (c.status & 64) = 64 THEN 'FOREIGN KEY' + WHEN (c.status & 128) = 128 THEN 'CHECK' + ELSE 'CONSTRAINT' + END AS constraint_type, + cm.text AS check_clause, + 'Y' AS is_enforced +FROM dbo.sysconstraints c +JOIN dbo.sysobjects o ON c.tableid = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.sysobjects co ON c.constrid = co.id +LEFT JOIN dbo.syscomments cm ON co.id = cm.id AND cm.colid = 1 +WHERE o.name = :table_name + AND u.name = COALESCE(:schema_name, USER_NAME()) +ORDER BY co.name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql b/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql new file mode 100644 index 000000000..72e1a9e93 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql @@ -0,0 +1,43 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + co.name AS constraint_name, + fc.name AS column_name, + ru.name AS referenced_schema, + ro.name AS referenced_table, + rc.name AS referenced_column +FROM dbo.sysreferences r +JOIN dbo.sysobjects o ON r.tableid = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.sysobjects co ON r.constrid = co.id +JOIN dbo.sysobjects ro ON r.reftabid = ro.id +JOIN dbo.sysusers ru ON ro.uid = ru.uid +JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = r.fokey1 +JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = r.refkey1 +WHERE u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY u.name, o.name, co.name; + +-- name: by_table +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + co.name AS constraint_name, + fc.name AS column_name, + ru.name AS referenced_schema, + ro.name AS referenced_table, + rc.name AS referenced_column +FROM dbo.sysreferences r +JOIN dbo.sysobjects o ON r.tableid = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +JOIN dbo.sysobjects co ON r.constrid = co.id +JOIN dbo.sysobjects ro ON r.reftabid = ro.id +JOIN dbo.sysusers ru ON ro.uid = ru.uid +JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = r.fokey1 +JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = r.refkey1 +WHERE o.name = :table_name + AND u.name = COALESCE(:schema_name, USER_NAME()) +ORDER BY co.name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql b/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql new file mode 100644 index 000000000..58c15fd02 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql @@ -0,0 +1,37 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + i.name AS index_name, + index_col(u.name + '.' + o.name, i.indid, 1) AS column_name, + 1 AS column_position, + CASE WHEN (i.status & 2) = 2 THEN 1 ELSE 0 END AS is_unique, + CASE WHEN (i.status & 2048) = 2048 THEN 1 ELSE 0 END AS is_primary +FROM dbo.sysindexes i +JOIN dbo.sysobjects o ON i.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND i.indid BETWEEN 1 AND 254 + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY u.name, o.name, i.name; + +-- name: by_table +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + i.name AS index_name, + index_col(u.name + '.' + o.name, i.indid, 1) AS column_name, + 1 AS column_position, + CASE WHEN (i.status & 2) = 2 THEN 1 ELSE 0 END AS is_unique, + CASE WHEN (i.status & 2048) = 2048 THEN 1 ELSE 0 END AS is_primary +FROM dbo.sysindexes i +JOIN dbo.sysobjects o ON i.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND i.indid BETWEEN 1 AND 254 + AND o.name = :table_name + AND u.name = COALESCE(:schema_name, USER_NAME()) +ORDER BY i.name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/objects.sql b/sqlspec/data_dictionary/dialects/sybase/sql/objects.sql new file mode 100644 index 000000000..7dcdd41bb --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/objects.sql @@ -0,0 +1,21 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS object_name, + CASE o.type + WHEN 'U' THEN 'TABLE' + WHEN 'V' THEN 'VIEW' + WHEN 'P' THEN 'PROCEDURE' + WHEN 'SF' THEN 'FUNCTION' + WHEN 'TR' THEN 'TRIGGER' + ELSE o.type + END AS object_type, + o.crdate AS created, + NULL AS remarks +FROM dbo.sysobjects o +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type IN ('U', 'V', 'P', 'SF', 'TR') + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:object_name IS NULL OR o.name = :object_name) +ORDER BY u.name, object_type, o.name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/schemas.sql b/sqlspec/data_dictionary/dialects/sybase/sql/schemas.sql new file mode 100644 index 000000000..83379acae --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/schemas.sql @@ -0,0 +1,16 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + u.name AS owner_name, + NULL AS create_time, + NULL AS comment +FROM dbo.sysusers u +WHERE u.uid < 16384 + AND u.name NOT IN ('public', 'probe', 'guest') + AND (:schema_name IS NULL OR u.name = :schema_name) +ORDER BY u.name; + +-- name: current +-- dialect: sybase +SELECT USER_NAME() AS schema_name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql b/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql new file mode 100644 index 000000000..a91957f04 --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql @@ -0,0 +1,62 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + 'BASE TABLE' AS table_type, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysreferences r + WHERE r.tableid = o.id + AND r.reftabid <> o.id + ) THEN 1 + ELSE 0 + END AS dependency_level, + CASE + WHEN EXISTS ( + SELECT 1 + FROM dbo.sysreferences r + WHERE r.tableid = o.id + AND r.reftabid <> o.id + ) THEN 1 + ELSE 0 + END AS level +FROM dbo.sysobjects o +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY dependency_level, u.name, o.name; + +-- name: all_by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS table_name, + CASE o.type + WHEN 'U' THEN 'BASE TABLE' + WHEN 'V' THEN 'VIEW' + WHEN 'S' THEN 'SYSTEM TABLE' + ELSE 'TABLE' + END AS table_type, + 0 AS dependency_level, + 0 AS level, + NULL AS estimated_row_count, + NULL AS table_comment +FROM dbo.sysobjects o +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type IN ('U', 'V') + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) +ORDER BY u.name, o.name; + +-- name: names_by_schema +-- dialect: sybase +SELECT + o.name AS table_name +FROM dbo.sysobjects o +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND u.name = COALESCE(:schema_name, USER_NAME()) +ORDER BY o.name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/version.sql b/sqlspec/data_dictionary/dialects/sybase/sql/version.sql new file mode 100644 index 000000000..fd62b988a --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/version.sql @@ -0,0 +1,3 @@ +-- name: current +-- dialect: sybase +SELECT @@version AS version_string; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/views.sql b/sqlspec/data_dictionary/dialects/sybase/sql/views.sql new file mode 100644 index 000000000..03472e96f --- /dev/null +++ b/sqlspec/data_dictionary/dialects/sybase/sql/views.sql @@ -0,0 +1,15 @@ +-- name: by_schema +-- dialect: sybase +SELECT + u.name AS schema_name, + o.name AS view_name, + cm.text AS definition, + 0 AS is_read_only, + 1 AS is_valid +FROM dbo.sysobjects o +JOIN dbo.sysusers u ON o.uid = u.uid +LEFT JOIN dbo.syscomments cm ON o.id = cm.id AND cm.colid = 1 +WHERE o.type = 'V' + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:view_name IS NULL OR o.name = :view_name) +ORDER BY u.name, o.name; diff --git a/sqlspec/utils/fixtures.py b/sqlspec/utils/fixtures.py index 17b834f42..5774fa84f 100644 --- a/sqlspec/utils/fixtures.py +++ b/sqlspec/utils/fixtures.py @@ -90,6 +90,7 @@ "blob", "bytea", "image", + "long binary", "long raw", "longblob", "mediumblob", @@ -117,6 +118,8 @@ "mysql": frozenset({"blob", "geometry", "json", "longblob", "mediumblob", "point", "tinyblob", "vector"}), "oracle": frozenset({"bfile", "blob", "clob", "long", "long raw", "nclob", "vector", "xmltype"}), "spanner": frozenset({"json", "tokenlist"}), + "sqlanywhere": frozenset({"long binary", "long nvarchar", "long varchar", "xml"}), + "sybase": frozenset({"image", "long binary", "long nvarchar", "long varchar", "text", "unitext"}), "tsql": frozenset({"geography", "geometry", "image", "ntext", "text", "xml"}), } _IDENTITY_GENERATION_VALUES: Final["frozenset[str]"] = frozenset({"always", "by default", "by default on null"}) @@ -883,11 +886,15 @@ def _dialect_name(dialect: "DialectType") -> str: def _dialect_family(dialect_name: str) -> str: - """Return ``postgres`` or ``mysql`` for those dialect families, otherwise the dialect name.""" + """Return canonical dialect family for a dialect name.""" if dialect_name in _POSTGRES_DIALECTS: return "postgres" if dialect_name in _MYSQL_DIALECTS: return "mysql" + if dialect_name == "ase": + return "sybase" + if dialect_name == "sa": + return "sqlanywhere" return dialect_name @@ -1598,17 +1605,17 @@ def _column_value_decoder(family: str, data_type: str) -> "Callable[[Any], Any] return encode_json if family in _JSON_VALUE_FAMILIES else None if family == "sqlite": return None - if data_type.startswith(("timestamp", "datetime")): + if data_type.startswith(("timestamp", "datetime", "bigdatetime", "smalldatetime", "datetimetz")): return _decode_datetime if data_type == "date": return _decode_datetime if family == "oracle" else _decode_date - if data_type.startswith("time") and family != "mysql": + if data_type.startswith(("time", "bigtime")) and family != "mysql": return _decode_time if data_type.startswith("interval"): return _decode_duckdb_interval if family == "duckdb" else _decode_interval - if data_type.startswith(("numeric", "decimal")): + if data_type.startswith(("numeric", "decimal", "money", "smallmoney")): return _decode_decimal - if data_type == "uuid": + if data_type in {"uuid", "uniqueidentifier"}: return _decode_uuid return None @@ -1716,14 +1723,16 @@ def _table_insert_statement( def _identity_insert_statements( dialect: "DialectType", family: str, table: str, columns: "list[str]", table_columns: "list[_TableColumn]" ) -> "tuple[str, str] | None": - """Return the SQL Server statements that allow and then disallow explicit identity values. + """Return the SQL Server and Sybase ASE statements that allow and then disallow explicit identity values. - SQL Server rejects values for identity columns unless ``IDENTITY_INSERT`` is on for + SQL Server and Sybase ASE reject values for identity columns unless ``IDENTITY_INSERT`` is on for the table, so inserts that include an identity column run between these statements. Other dialects return ``None``. """ inserted = set(columns) - if family != "tsql" or not any(column.is_identity and column.name in inserted for column in table_columns): + if family not in {"tsql", "sybase"} or not any( + column.is_identity and column.name in inserted for column in table_columns + ): return None table_sql = _quoted_table(table).sql(dialect=_sqlglot_dialect(dialect, family)) return f"SET IDENTITY_INSERT {table_sql} ON", f"SET IDENTITY_INSERT {table_sql} OFF" @@ -1826,6 +1835,7 @@ def _table_export_query(dialect: "DialectType", family: str, table: str, table_c order_columns = [column.name for column in table_columns if column.is_primary] or [ column.name for column in table_columns if _is_orderable_type(family, column.data_type) ] + ] order_by: list[exp.Expr] = [exp.Column(this=_quoted_identifier(name)) for name in order_columns] if not order_by and not table_columns: order_by = [exp.Literal.number(1)] @@ -1996,6 +2006,32 @@ def _resync_table_sequences_sync( driver: "SyncDriverAdapterBase", family: str, dialect_name: str, table: str, columns: "list[_TableColumn]" ) -> None: """Resynchronize sequences, identity columns, or auto-increment counters for a table synchronously.""" + if family == "sybase": + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = driver.select_value(query) + next_val = int(max_val or 0) + if next_val > 0: + escaped_table = table.replace("'", "''") + driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + return + + if family == "sqlanywhere": + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = driver.select_value(query) + next_val = int(max_val or 0) + schema_part, _, table_part = table.rpartition(".") + escaped_table = table_part.replace("'", "''") + escaped_schema = schema_part.replace("'", "''") + schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" + driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + return + if family == "db2": quoted_table = _quoted_table_name(table) for column in columns: @@ -2096,6 +2132,32 @@ async def _resync_table_sequences_async( driver: "AsyncDriverAdapterBase", family: str, dialect_name: str, table: str, columns: "list[_TableColumn]" ) -> None: """Resynchronize sequences, identity columns, or auto-increment counters for a table asynchronously.""" + if family == "sybase": + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = await driver.select_value(query) + next_val = int(max_val or 0) + if next_val > 0: + escaped_table = table.replace("'", "''") + await driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + return + + if family == "sqlanywhere": + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = await driver.select_value(query) + next_val = int(max_val or 0) + schema_part, _, table_part = table.rpartition(".") + escaped_table = table_part.replace("'", "''") + escaped_schema = schema_part.replace("'", "''") + schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" + await driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + return + if family == "db2": quoted_table = _quoted_table_name(table) for column in columns: diff --git a/tests/unit/data_dictionary/test_sybase_sqlanywhere_dialects.py b/tests/unit/data_dictionary/test_sybase_sqlanywhere_dialects.py new file mode 100644 index 000000000..a2a72c5dd --- /dev/null +++ b/tests/unit/data_dictionary/test_sybase_sqlanywhere_dialects.py @@ -0,0 +1,162 @@ +"""Unit tests for Sybase ASE and SAP SQL Anywhere data dictionary configurations and SQL packs.""" + +import pytest + +from sqlspec.data_dictionary import DataDictionaryLoader, TableMetadata, VersionInfo, get_dialect_config +from sqlspec.data_dictionary.dialects import SQLANYWHERE_CONFIG, SYBASE_CONFIG +from sqlspec.data_dictionary.dialects.sqlanywhere import ( + SQLANYWHERE_METADATA_DOMAINS, + SqlanywhereVersionInfo, + build_sqlanywhere_metadata_capability_profile, + extract_sqlanywhere_version_value, + list_sqlanywhere_available_features, + merge_sqlanywhere_table_lists, + resolve_sqlanywhere_feature_flag, +) +from sqlspec.data_dictionary.dialects.sybase import ( + SYBASE_METADATA_DOMAINS, + SybaseVersionInfo, + build_sybase_metadata_capability_profile, + extract_sybase_version_value, + list_sybase_available_features, + merge_sybase_table_lists, + resolve_sybase_feature_flag, +) + + +def test_sybase_and_sqlanywhere_dialect_configs_registered() -> None: + """Both sybase and sqlanywhere dialect configs should resolve by canonical name and alias.""" + assert get_dialect_config("sybase") is SYBASE_CONFIG + assert get_dialect_config("ase") is SYBASE_CONFIG + assert get_dialect_config("sqlanywhere") is SQLANYWHERE_CONFIG + assert get_dialect_config("sa") is SQLANYWHERE_CONFIG + + assert SYBASE_CONFIG.name == "sybase" + assert SYBASE_CONFIG.default_schema == "dbo" + assert SYBASE_CONFIG.get_feature_flag("supports_cte") is False + assert SYBASE_CONFIG.get_feature_flag("supports_window_functions") is False + assert SYBASE_CONFIG.get_feature_flag("supports_merge") is False + assert SYBASE_CONFIG.get_feature_flag("supports_returning") is False + assert SYBASE_CONFIG.get_feature_flag("supports_transactional_ddl") is False + assert SYBASE_CONFIG.get_feature_flag("supports_transactions") is True + assert SYBASE_CONFIG.get_optimal_type("boolean") == "BIT" + assert SYBASE_CONFIG.get_optimal_type("timestamp") == "BIGDATETIME" + assert SYBASE_CONFIG.get_optimal_type("blob") == "IMAGE" + + assert SQLANYWHERE_CONFIG.name == "sqlanywhere" + assert SQLANYWHERE_CONFIG.default_schema == "DBA" + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_cte") is True + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_window_functions") is True + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_merge") is True + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_for_update") is True + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_skip_locked") is True + assert SQLANYWHERE_CONFIG.get_feature_flag("supports_transactional_ddl") is True + assert SQLANYWHERE_CONFIG.get_optimal_type("uuid") == "UNIQUEIDENTIFIER" + assert SQLANYWHERE_CONFIG.get_optimal_type("text") == "LONG VARCHAR" + assert SQLANYWHERE_CONFIG.get_optimal_type("blob") == "LONG BINARY" + + +def test_sybase_and_sqlanywhere_version_patterns() -> None: + """Version regexes should extract major, minor, and patch components for ASE and SQL Anywhere.""" + ase_banner = ( + "Adaptive Server Enterprise/16.0 SP04 PL04/EBF 30626 SMP/P/x86_64/" + "Enterprise Linux/ase160sp04pl04x/0/64-bit/FBO/Thu" + ) + ase_match = SYBASE_CONFIG.version_pattern.search(ase_banner) + assert ase_match is not None + assert tuple(int(part or 0) for part in ase_match.groups()) == (16, 0, 4) + + sa_banner = "17.0.11.7458" + sa_match = SQLANYWHERE_CONFIG.version_pattern.search(sa_banner) + assert sa_match is not None + assert tuple(int(part or 0) for part in sa_match.groups()) == (17, 0, 11) + + +def test_sybase_and_sqlanywhere_reflection_helpers() -> None: + """Reflection helpers should extract version values, resolve features, and merge table lists.""" + ase_version = SybaseVersionInfo(16, 0, 4, "SP04") + assert str(ase_version) == "16.0.4 (SP04)" + assert extract_sybase_version_value({"version_string": "Adaptive Server Enterprise/16.0 SP04"}) == ( + "Adaptive Server Enterprise/16.0 SP04" + ) + assert resolve_sybase_feature_flag("supports_transactions", ase_version) is True + assert resolve_sybase_feature_flag("supports_cte", ase_version) is False + assert "supports_transactions" in list_sybase_available_features() + + sa_version = SqlanywhereVersionInfo(17, 0, 11, "7458") + assert str(sa_version) == "17.0.11 (7458)" + assert extract_sqlanywhere_version_value({"version_string": "17.0.11.7458"}) == "17.0.11.7458" + assert resolve_sqlanywhere_feature_flag("supports_cte", sa_version) is True + assert resolve_sqlanywhere_feature_flag("supports_window_functions", VersionInfo(12, 0, 0)) is True + assert "supports_cte" in list_sqlanywhere_available_features() + + ordered: list[TableMetadata] = [{"schema_name": "dbo", "table_name": "users"}] + all_rows: list[TableMetadata] = [ + {"schema_name": "dbo", "table_name": "posts"}, + {"schema_name": "dbo", "table_name": "users"}, + ] + assert merge_sybase_table_lists(ordered, all_rows) == [ + {"schema_name": "dbo", "table_name": "users"}, + {"schema_name": "dbo", "table_name": "posts"}, + ] + assert merge_sqlanywhere_table_lists(ordered, all_rows) == [ + {"schema_name": "dbo", "table_name": "users"}, + {"schema_name": "dbo", "table_name": "posts"}, + ] + + sybase_profile = build_sybase_metadata_capability_profile("sybase") + assert sybase_profile.dialect == "sybase" + assert len(sybase_profile.capabilities) == len(SYBASE_METADATA_DOMAINS) + + sa_profile = build_sqlanywhere_metadata_capability_profile("sqlanywhere") + assert sa_profile.dialect == "sqlanywhere" + assert len(sa_profile.capabilities) == len(SQLANYWHERE_METADATA_DOMAINS) + + +@pytest.mark.parametrize("dialect", ["sybase", "sqlanywhere"]) +@pytest.mark.parametrize( + ("domain", "query_names"), + [ + ("version", ("current",)), + ("schemas", ("by_schema", "current")), + ("tables", ("by_schema", "all_by_schema", "names_by_schema")), + ("columns", ("by_schema", "by_table")), + ("indexes", ("by_schema", "by_table")), + ("foreign_keys", ("by_schema", "by_table")), + ("constraints", ("by_schema", "by_table")), + ("objects", ("by_schema",)), + ("views", ("by_schema",)), + ], +) +def test_all_domain_queries_load_for_sybase_and_sqlanywhere( + dialect: str, domain: str, query_names: tuple[str, ...] +) -> None: + """DataDictionaryLoader should load all 9 domains and named queries for sybase and sqlanywhere.""" + loader = DataDictionaryLoader() + assert loader.has_domain_queries(dialect, domain) is True + for query_name in query_names: + query = loader.get_domain_query(dialect, domain, query_name) + assert query.is_supported is True + assert query.query_text is not None + assert query.query_text.strip() != "" + + +def test_sybase_and_sqlanywhere_catalog_tables_referenced() -> None: + """Sybase queries should use dbo.sys* catalogs and SQL Anywhere queries should use SYS.SYS* catalogs.""" + loader = DataDictionaryLoader() + + sybase_tables = loader.get_domain_query_text("sybase", "tables", "by_schema") or "" + sybase_columns = loader.get_domain_query_text("sybase", "columns", "by_table") or "" + sybase_fks = loader.get_domain_query_text("sybase", "foreign_keys", "by_table") or "" + assert "dbo.sysobjects" in sybase_tables + assert "dbo.syscolumns" in sybase_columns + assert "dbo.systypes" in sybase_columns + assert "dbo.sysreferences" in sybase_fks + + sa_tables = loader.get_domain_query_text("sqlanywhere", "tables", "by_schema") or "" + sa_columns = loader.get_domain_query_text("sqlanywhere", "columns", "by_table") or "" + sa_fks = loader.get_domain_query_text("sqlanywhere", "foreign_keys", "by_table") or "" + assert "SYS.SYSTAB" in sa_tables + assert "SYS.SYSTABCOL" in sa_columns + assert "SYS.SYSDOMAIN" in sa_columns + assert "SYS.SYSFKEY" in sa_fks diff --git a/tests/unit/utils/test_fixtures_sybase.py b/tests/unit/utils/test_fixtures_sybase.py new file mode 100644 index 000000000..35a5e6723 --- /dev/null +++ b/tests/unit/utils/test_fixtures_sybase.py @@ -0,0 +1,167 @@ +"""Unit tests for Sybase ASE and SAP SQL Anywhere fixture loading, decoding, and identity resync.""" + +import json +from datetime import UTC, datetime, time +from decimal import Decimal +from pathlib import Path +from typing import Any +from unittest.mock import AsyncMock, MagicMock +from uuid import UUID + +from sqlspec.utils.fixtures import export_table_fixtures_sync, load_table_fixtures_async, load_table_fixtures_sync + + +def _mock_sync_driver(dialect: str, columns: list[dict[str, Any]], max_identity: int = 42) -> MagicMock: + driver = MagicMock() + driver.statement_config.dialect = dialect + driver.data_dictionary.get_columns.return_value = columns + driver.select_value_or_none.return_value = max_identity + return driver + + +def _mock_async_driver(dialect: str, columns: list[dict[str, Any]], max_identity: int = 42) -> MagicMock: + driver = MagicMock() + driver.statement_config.dialect = dialect + driver.data_dictionary.get_columns = AsyncMock(return_value=columns) + driver.execute_many = AsyncMock() + driver.execute = AsyncMock() + driver.select_value_or_none = AsyncMock(return_value=max_identity) + return driver + + +SYBASE_IDENTITY_COLUMNS: list[dict[str, Any]] = [ + {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "YES", "is_generated": 0}, + { + "column_name": "name", + "data_type": "varchar", + "is_primary": False, + "identity_generation": None, + "is_generated": 0, + }, +] + +SQLANYWHERE_IDENTITY_COLUMNS: list[dict[str, Any]] = [ + { + "column_name": "id", + "data_type": "integer", + "is_primary": True, + "identity_generation": "AUTOINCREMENT", + "is_generated": 0, + }, + { + "column_name": "name", + "data_type": "varchar", + "is_primary": False, + "identity_generation": None, + "is_generated": 0, + }, +] + + +def test_sybase_identity_resync_executes_sp_chgattribute_sync(tmp_path: Path) -> None: + """Sybase fixture load with resync_sequences=True should execute sp_chgattribute when max > 0.""" + rows = [{"id": 42, "name": "Alice"}] + (tmp_path / "users.json").write_text(json.dumps(rows), encoding="utf-8") + driver = _mock_sync_driver("sybase", SYBASE_IDENTITY_COLUMNS, max_identity=42) + + counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) + + assert counts == {"users": 1} + assert driver.execute_many.call_args.args[1] == [{"id": 42, "name": "Alice"}] + driver.select_value_or_none.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') + driver.execute.assert_called_once_with("EXEC sp_chgattribute 'users', 'identity_burn_max', 0, '42'") + + +def test_sybase_identity_resync_skips_when_max_is_zero(tmp_path: Path) -> None: + """Sybase fixture load should skip sp_chgattribute when table max is 0.""" + (tmp_path / "users.json").write_text("[]", encoding="utf-8") + driver = _mock_sync_driver("sybase", SYBASE_IDENTITY_COLUMNS, max_identity=0) + + counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) + + assert counts == {"users": 0} + driver.select_value_or_none.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') + driver.execute.assert_not_called() + + +async def test_sqlanywhere_identity_resync_executes_sa_reset_identity_async(tmp_path: Path) -> None: + """SQL Anywhere async fixture load should execute CALL sa_reset_identity with table, schema, and max.""" + rows = [{"id": 15, "name": "Bob"}] + (tmp_path / "DBA.users.json").write_text(json.dumps(rows), encoding="utf-8") + driver = _mock_async_driver("sqlanywhere", SQLANYWHERE_IDENTITY_COLUMNS, max_identity=15) + + counts = await load_table_fixtures_async(driver, tmp_path, resync_sequences=True) + + assert counts == {"DBA.users": 1} + assert driver.execute_many.call_args.args[1] == [{"id": 15, "name": "Bob"}] + driver.select_value_or_none.assert_awaited_once_with('SELECT coalesce(max("id"), 0) FROM "DBA"."users"') + driver.execute.assert_awaited_once_with("CALL sa_reset_identity('users', 'DBA', 15)") + + +def test_sybase_and_sqlanywhere_lob_types_excluded_from_export_order_by(tmp_path: Path) -> None: + """Unorderable LOB types in Sybase and SQL Anywhere should be excluded from export ORDER BY.""" + driver = MagicMock() + driver.statement_config.dialect = "sybase" + driver.data_dictionary.get_columns.return_value = [ + {"column_name": "code", "data_type": "varchar", "is_primary": False}, + {"column_name": "notes_text", "data_type": "text", "is_primary": False}, + {"column_name": "notes_unitext", "data_type": "unitext", "is_primary": False}, + {"column_name": "photo", "data_type": "image", "is_primary": False}, + {"column_name": "sa_clob", "data_type": "long varchar", "is_primary": False}, + {"column_name": "sa_nclob", "data_type": "long nvarchar", "is_primary": False}, + {"column_name": "sa_blob", "data_type": "long binary", "is_primary": False}, + ] + driver.select.return_value = [] + + export_table_fixtures_sync(driver, tmp_path, ["docs"], compress=False) + + select_query = driver.select.call_args.args[0] + order_by_clause = select_query.to_statement().sql.partition("ORDER BY")[2] + assert "[code]" in order_by_clause or '"code"' in order_by_clause + for lob_col in ("notes_text", "notes_unitext", "photo", "sa_clob", "sa_nclob", "sa_blob"): + assert lob_col not in order_by_clause + + +def test_sybase_and_sqlanywhere_value_decoders(tmp_path: Path) -> None: + """Sybase and SQL Anywhere temporal, monetary, UUID, and binary column types should decode on load.""" + rows = [ + { + "id": 1, + "big_dt": "2026-10-30T12:34:56.123456+00:00", + "small_dt": "2026-10-30T12:34:00+00:00", + "dt_tz": "2026-10-30T12:34:56+00:00", + "big_t": "12:34:56.123456", + "price": "19.95", + "small_price": "5.50", + "ext_id": "7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f", + "raw_image": "AP8=", + "raw_long_bin": "AP8=", + } + ] + (tmp_path / "records.json").write_text(json.dumps(rows), encoding="utf-8") + columns = [ + {"column_name": "id", "data_type": "int", "is_primary": True}, + {"column_name": "big_dt", "data_type": "bigdatetime", "is_primary": False}, + {"column_name": "small_dt", "data_type": "smalldatetime", "is_primary": False}, + {"column_name": "dt_tz", "data_type": "datetimetz", "is_primary": False}, + {"column_name": "big_t", "data_type": "bigtime", "is_primary": False}, + {"column_name": "price", "data_type": "money", "is_primary": False}, + {"column_name": "small_price", "data_type": "smallmoney", "is_primary": False}, + {"column_name": "ext_id", "data_type": "uniqueidentifier", "is_primary": False}, + {"column_name": "raw_image", "data_type": "image", "is_primary": False}, + {"column_name": "raw_long_bin", "data_type": "long binary", "is_primary": False}, + ] + driver = _mock_sync_driver("sybase", columns) + + load_table_fixtures_sync(driver, tmp_path) + + loaded_row = driver.execute_many.call_args.args[1][0] + assert loaded_row["big_dt"] == datetime(2026, 10, 30, 12, 34, 56, 123456, tzinfo=UTC) + assert loaded_row["small_dt"] == datetime(2026, 10, 30, 12, 34, 0, tzinfo=UTC) + assert loaded_row["dt_tz"] == datetime(2026, 10, 30, 12, 34, 56, tzinfo=UTC) + assert loaded_row["big_t"] == time(12, 34, 56, 123456) + assert loaded_row["price"] == Decimal("19.95") + assert loaded_row["small_price"] == Decimal("5.50") + assert loaded_row["ext_id"] == UUID("7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f") + assert loaded_row["raw_image"] == b"\x00\xff" + assert loaded_row["raw_long_bin"] == b"\x00\xff" From 2dfafec047a40cfbac34ede51e9fc997d0befcf8 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 18:07:53 +0000 Subject: [PATCH 26/35] feat(sybase): add sap ase adapter over pymssql tds 5.0 and extension stores --- sqlspec/adapters/sybase/__init__.py | 37 + sqlspec/adapters/sybase/_typing.py | 131 +++ sqlspec/adapters/sybase/adk/__init__.py | 5 + sqlspec/adapters/sybase/adk/store.py | 929 +++++++++++++++++++ sqlspec/adapters/sybase/config.py | 183 ++++ sqlspec/adapters/sybase/core.py | 411 ++++++++ sqlspec/adapters/sybase/data_dictionary.py | 275 ++++++ sqlspec/adapters/sybase/driver.py | 385 ++++++++ sqlspec/adapters/sybase/events/__init__.py | 5 + sqlspec/adapters/sybase/events/store.py | 55 ++ sqlspec/adapters/sybase/litestar/__init__.py | 5 + sqlspec/adapters/sybase/litestar/store.py | 241 +++++ sqlspec/adapters/sybase/migrations.py | 166 ++++ sqlspec/adapters/sybase/pool.py | 265 ++++++ tests/unit/adapters/test_sybase.py | 517 +++++++++++ 15 files changed, 3610 insertions(+) create mode 100644 sqlspec/adapters/sybase/__init__.py create mode 100644 sqlspec/adapters/sybase/_typing.py create mode 100644 sqlspec/adapters/sybase/adk/__init__.py create mode 100644 sqlspec/adapters/sybase/adk/store.py create mode 100644 sqlspec/adapters/sybase/config.py create mode 100644 sqlspec/adapters/sybase/core.py create mode 100644 sqlspec/adapters/sybase/data_dictionary.py create mode 100644 sqlspec/adapters/sybase/driver.py create mode 100644 sqlspec/adapters/sybase/events/__init__.py create mode 100644 sqlspec/adapters/sybase/events/store.py create mode 100644 sqlspec/adapters/sybase/litestar/__init__.py create mode 100644 sqlspec/adapters/sybase/litestar/store.py create mode 100644 sqlspec/adapters/sybase/migrations.py create mode 100644 sqlspec/adapters/sybase/pool.py create mode 100644 tests/unit/adapters/test_sybase.py diff --git a/sqlspec/adapters/sybase/__init__.py b/sqlspec/adapters/sybase/__init__.py new file mode 100644 index 000000000..f86b75c56 --- /dev/null +++ b/sqlspec/adapters/sybase/__init__.py @@ -0,0 +1,37 @@ +"""Sybase ASE adapter for SQLSpec.""" + +from sqlspec.adapters.sybase._typing import SybaseConnection, SybaseCursor +from sqlspec.adapters.sybase.config import SybaseConfig, SybaseConnectionParams, SybaseDriverFeatures, SybasePoolParams +from sqlspec.adapters.sybase.core import ( + DEFAULT_SYBASE_CONN_PROPERTIES, + DEFAULT_SYBASE_TDS_VERSION, + build_connection_config, + create_mapped_exception, + default_statement_config, + driver_profile, +) +from sqlspec.adapters.sybase.data_dictionary import SybaseDataDictionary, SybaseSyncDataDictionary +from sqlspec.adapters.sybase.driver import SybaseDriver, SybaseExceptionHandler +from sqlspec.adapters.sybase.migrations import SybaseSyncMigrationTracker +from sqlspec.adapters.sybase.pool import SybaseConnectionPool + +__all__ = ( + "DEFAULT_SYBASE_CONN_PROPERTIES", + "DEFAULT_SYBASE_TDS_VERSION", + "SybaseConfig", + "SybaseConnection", + "SybaseConnectionParams", + "SybaseConnectionPool", + "SybaseCursor", + "SybaseDataDictionary", + "SybaseDriver", + "SybaseDriverFeatures", + "SybaseExceptionHandler", + "SybasePoolParams", + "SybaseSyncDataDictionary", + "SybaseSyncMigrationTracker", + "build_connection_config", + "create_mapped_exception", + "default_statement_config", + "driver_profile", +) diff --git a/sqlspec/adapters/sybase/_typing.py b/sqlspec/adapters/sybase/_typing.py new file mode 100644 index 000000000..2d2013d4f --- /dev/null +++ b/sqlspec/adapters/sybase/_typing.py @@ -0,0 +1,131 @@ +"""Sybase ASE adapter type definitions. + +This module contains type aliases and classes that are excluded from mypyc +compilation to avoid ABI boundary issues. +""" + +import contextlib +from typing import TYPE_CHECKING, Any + +import pymssql as _pymssql +from mypy_extensions import mypyc_attr +from pymssql import Connection as _SybaseConnection +from pymssql import Cursor as _SybaseRawCursor +from pymssql import Error as SybaseError + +from sqlspec.utils.module_loader import import_optional_attr + +PYMSSQL_MODULE = _pymssql +_DRIVER_CLS: Any = None + +if TYPE_CHECKING: + from collections.abc import Callable + from types import TracebackType + from typing import TypeAlias + + from pymssql._pymssql import QueryParams as SybaseQueryParams + + from sqlspec.adapters.sybase.driver import SybaseDriver + from sqlspec.core import StatementConfig + + SybaseConnection: TypeAlias = _SybaseConnection + SybaseRawCursor: TypeAlias = _SybaseRawCursor + +if not TYPE_CHECKING: + SybaseQueryParams = Any + SybaseConnection = _SybaseConnection + SybaseRawCursor = _SybaseRawCursor + +__all__ = ( + "PYMSSQL_MODULE", + "SybaseConnection", + "SybaseCursor", + "SybaseError", + "SybaseQueryParams", + "SybaseRawCursor", + "SybaseSessionContext", +) + + +class SybaseCursor: + """Context manager for Sybase pymssql cursor operations.""" + + __slots__ = ("_as_dict", "connection", "cursor") + + def __init__(self, connection: "SybaseConnection", *, as_dict: bool | None = None) -> None: + self.connection = connection + self.cursor: SybaseRawCursor | None = None + self._as_dict = as_dict + + def __enter__(self) -> "SybaseRawCursor": + if self._as_dict is None: + self.cursor = self.connection.cursor() + else: + try: + raw_cursor: Any = ( + self.connection.cursor(as_dict=True) if self._as_dict else self.connection.cursor(as_dict=False) + ) + self.cursor = raw_cursor + except TypeError: + self.cursor = self.connection.cursor() + assert self.cursor is not None + return self.cursor + + def __exit__(self, *_: Any) -> None: + if self.cursor is not None: + with contextlib.suppress(Exception): + self.cursor.close() + + +@mypyc_attr(allow_interpreted_subclasses=True, native_class=False) +class SybaseSessionContext: + """Sync context manager for Sybase ASE sessions.""" + + __slots__ = ( + "_acquire_connection", + "_connection", + "_driver", + "_driver_features", + "_prepare_driver", + "_release_connection", + "_statement_config", + ) + + def __init__( + self, + acquire_connection: "Callable[[], Any]", + release_connection: "Callable[..., Any]", + statement_config: "StatementConfig", + driver_features: "dict[str, Any]", + prepare_driver: "Callable[[SybaseDriver], SybaseDriver]", + ) -> None: + self._acquire_connection = acquire_connection + self._release_connection = release_connection + self._statement_config = statement_config + self._driver_features = driver_features + self._prepare_driver = prepare_driver + self._connection: Any = None + self._driver: SybaseDriver | None = None + + def __enter__(self) -> "SybaseDriver": + global _DRIVER_CLS + if _DRIVER_CLS is None: + _DRIVER_CLS = import_optional_attr("sqlspec.adapters.sybase.driver", "SybaseDriver") + self._connection = self._acquire_connection() + self._driver = _DRIVER_CLS( + connection=self._connection, statement_config=self._statement_config, driver_features=self._driver_features + ) + assert self._driver is not None + return self._prepare_driver(self._driver) + + def __exit__( + self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None" + ) -> "bool | None": + if self._driver is not None and (exc_type is not None or self._driver._connection_in_transaction()): + with contextlib.suppress(Exception): + self._driver.rollback() + if self._connection is not None: + self._release_connection(self._connection, exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb) + self._connection = None + self._driver = None + return None diff --git a/sqlspec/adapters/sybase/adk/__init__.py b/sqlspec/adapters/sybase/adk/__init__.py new file mode 100644 index 000000000..c134a08d0 --- /dev/null +++ b/sqlspec/adapters/sybase/adk/__init__.py @@ -0,0 +1,5 @@ +"""Sybase ASE ADK extension.""" + +from sqlspec.adapters.sybase.adk.store import SybaseADKConfig, SybaseADKMemoryStore, SybaseADKStore + +__all__ = ("SybaseADKConfig", "SybaseADKMemoryStore", "SybaseADKStore") diff --git a/sqlspec/adapters/sybase/adk/store.py b/sqlspec/adapters/sybase/adk/store.py new file mode 100644 index 000000000..d88720ecc --- /dev/null +++ b/sqlspec/adapters/sybase/adk/store.py @@ -0,0 +1,929 @@ +"""Sybase ASE ADK stores for Google Agent Development Kit session storage.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, cast + +from sqlspec.adapters.sybase._typing import SybaseCursor, SybaseError +from sqlspec.adapters.sybase.core import ( + escape_sql_literal, + extract_error_number, + quote_bracket_identifier, + resolve_rowcount, + split_schema_and_table, +) +from sqlspec.config import ADKConfig +from sqlspec.extensions.adk import BaseSyncADKStore, StoredEvent, StoredSession, normalize_session_list_options +from sqlspec.extensions.adk.memory.store import BaseSyncADKMemoryStore +from sqlspec.utils.serializers import from_json, to_json + +if TYPE_CHECKING: + from collections.abc import Sequence + from datetime import timedelta + + from sqlspec.adapters.sybase.config import SybaseConfig + from sqlspec.adapters.sybase.driver import SybaseDriver + from sqlspec.extensions.adk import SessionOrderBy + from sqlspec.extensions.adk.memory._types import StoredMemory + +__all__ = ("SybaseADKConfig", "SybaseADKMemoryStore", "SybaseADKStore") + +SYBASE_TABLE_NOT_FOUND_ERROR: Final[int] = 208 +SYBASE_SCHEMA: Final[str] = "dbo" +JSON_COLUMN_TYPE: Final[str] = "TEXT" + + +class SybaseADKConfig(ADKConfig): + """Sybase ASE ADK extension settings.""" + + +class SybaseADKStore(BaseSyncADKStore["SybaseConfig"]): + """Synchronous Sybase ASE ADK session/event store.""" + + connector_name: ClassVar[str] = "sybase" + __slots__ = () + + def __init__(self, config: "SybaseConfig") -> None: + super().__init__(config) + + def create_tables(self) -> None: + """Create ADK tables (idempotent Sybase ASE DDL) and DD-gated indexes.""" + if not self.create_schema_enabled: + self.reconcile_schema() + return + + with self._config.provide_session() as driver: + driver.execute_script(self._sessions_table_ddl()) + driver.execute_script(self._events_table_ddl()) + driver.execute_script(self._app_states_table_ddl()) + driver.execute_script(self._user_states_table_ddl()) + driver.execute_script(self._metadata_table_ddl()) + index_specs = self._index_specs() + existing_indexes = _existing_index_names(driver, index_specs) + for index_name, index_table, columns in index_specs: + if _bare_name(index_name) not in existing_indexes: + driver.execute(_create_index_sql(index_table, index_name, columns)) + driver.commit() + self.reconcile_schema(assume_existing=True) + + def create_session( + self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None + ) -> StoredSession: + """Create a new ADK session.""" + owner_column = f", {quote_bracket_identifier(self._owner_id_column_name)}" if self._owner_id_column_name else "" + owner_param = ", %s" if self._owner_id_column_name else "" + insert_sql = f""" + INSERT INTO {_table_ref(self._session_table)} ( + id, app_name, user_id{owner_column}, state, create_time, update_time + ) + VALUES (%s, %s, %s{owner_param}, %s, GETUTCDATE(), GETUTCDATE()) + """ + select_sql = f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = %s AND user_id = %s AND id = %s + """ + params: tuple[Any, ...] + if self._owner_id_column_name: + params = (session_id, app_name, user_id, owner_id, to_json(state)) + else: + params = (session_id, app_name, user_id, to_json(state)) + self._execute(insert_sql, params, commit=True) + row = self._execute_fetchone(select_sql, (app_name, user_id, session_id)) + if row is None: + msg = "Failed to fetch created session" + raise RuntimeError(msg) + return _session_record_from_row(row) + + def get_session( + self, app_name: str, user_id: str, session_id: str, *, renew_for: "int | timedelta | None" = None + ) -> "StoredSession | None": + """Return a scoped session or ``None`` if absent.""" + try: + if renew_for is not None and self._calculate_expires_at(renew_for) is not None: + self._execute( + f""" + UPDATE {_table_ref(self._session_table)} + SET update_time = GETUTCDATE() + WHERE app_name = %s AND user_id = %s AND id = %s + """, + (app_name, user_id, session_id), + commit=True, + ) + row = self._execute_fetchone( + f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = %s AND user_id = %s AND id = %s + """, + (app_name, user_id, session_id), + ) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return None + raise + return _session_record_from_row(row) if row is not None else None + + def update_session_state(self, app_name: str, user_id: str, session_id: str, state: "dict[str, Any]") -> None: + """Replace a session's durable state.""" + self._execute( + f""" + UPDATE {_table_ref(self._session_table)} + SET state = %s, update_time = GETUTCDATE() + WHERE app_name = %s AND user_id = %s AND id = %s + """, + (to_json(state), app_name, user_id, session_id), + commit=True, + ) + + def list_sessions( + self, + app_name: str, + user_id: "str | None" = None, + *, + order_by: "SessionOrderBy" = "update_time", + descending: bool = True, + limit: "int | None" = None, + offset: "int | None" = None, + ) -> "list[StoredSession]": + """List ADK sessions for an application, optionally scoped to a user.""" + column, direction, page_limit, page_offset = normalize_session_list_options(order_by, descending, limit, offset) + if page_limit == 0: + return [] + + sql, params = _session_list_query( + self._session_table, app_name, user_id, column, direction, page_limit, page_offset + ) + try: + rows = self._execute_fetchall(sql, params) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return [] + raise + return [_session_record_from_row(row) for row in rows] + + def delete_session(self, app_name: str, user_id: str, session_id: str) -> None: + """Delete a session and its associated events.""" + self._execute( + f"DELETE FROM {_table_ref(self._events_table)} WHERE app_name = %s AND user_id = %s AND session_id = %s", + (app_name, user_id, session_id), + commit=True, + ) + self._execute( + f"DELETE FROM {_table_ref(self._session_table)} WHERE app_name = %s AND user_id = %s AND id = %s", + (app_name, user_id, session_id), + commit=True, + ) + + def append_event(self, event_record: StoredEvent) -> None: + """Append an event to a session.""" + self._execute(_insert_event_sql(self._events_table), _event_insert_params(event_record), commit=True) + + def append_event_and_update_state( + self, + event_record: StoredEvent, + app_name: str, + user_id: str, + session_id: str, + state: "dict[str, Any]", + *, + app_state: "dict[str, Any] | None" = None, + user_state: "dict[str, Any] | None" = None, + ) -> StoredSession: + """Atomically append an event and update durable session/scoped state.""" + update_sql = f""" + UPDATE {_table_ref(self._session_table)} + SET state = %s, update_time = GETUTCDATE() + WHERE app_name = %s AND user_id = %s AND id = %s + """ + select_sql = f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = %s AND user_id = %s AND id = %s + """ + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + try: + cursor.execute(update_sql, (to_json(state), app_name, user_id, session_id)) + cursor.execute(select_sql, (app_name, user_id, session_id)) + row = cursor.fetchone() + if row is None: + _raise_session_not_found(session_id) + cursor.execute(_insert_event_sql(self._events_table), _event_insert_params(event_record)) + if app_state is not None: + app_json = to_json(app_state) + cursor.execute(self._upsert_app_state_sql(), (app_name, app_json, app_name, app_name, app_json)) + if user_state is not None: + user_json = to_json(user_state) + cursor.execute( + self._upsert_user_state_sql(), + (app_name, user_id, user_json, app_name, user_id, app_name, user_id, user_json), + ) + except Exception: + conn.rollback() + raise + conn.commit() + return _session_record_from_row(row) + + def get_events( + self, + app_name: str, + user_id: str, + session_id: str, + after_timestamp: "datetime | None" = None, + limit: "int | None" = None, + ) -> "list[StoredEvent]": + """Return events for a scoped session ordered by event timestamp.""" + if limit == 0: + return [] + sql, params = self._events_query(app_name, user_id, session_id, after_timestamp, limit) + try: + rows = self._execute_fetchall(sql, params) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return [] + raise + return [_event_record_from_row(row) for row in rows] + + def delete_expired_events(self, before: datetime, app_name: "str | None" = None) -> int: + """Delete events older than ``before``.""" + sql = f"DELETE FROM {_table_ref(self._events_table)} WHERE timestamp < %s" + params: list[Any] = [before] + if app_name is not None: + sql += " AND app_name = %s" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return 0 + raise + + def delete_idle_sessions(self, updated_before: datetime, app_name: "str | None" = None) -> int: + """Delete sessions whose update_time is older than ``updated_before``.""" + sql = f"DELETE FROM {_table_ref(self._session_table)} WHERE update_time < %s" + params: list[Any] = [updated_before] + if app_name is not None: + sql += " AND app_name = %s" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return 0 + raise + + def delete_idle_user_states(self, updated_before: datetime, app_name: "str | None" = None) -> int: + """Delete user state rows whose update_time is older than ``updated_before``.""" + sql = f"DELETE FROM {_table_ref(self._user_state_table)} WHERE update_time < %s" + params: list[Any] = [updated_before] + if app_name is not None: + sql += " AND app_name = %s" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return 0 + raise + + def get_app_state(self, app_name: str) -> "dict[str, Any] | None": + """Return app-scoped state.""" + try: + row = self._execute_fetchone( + f"SELECT TOP 1 state FROM {_table_ref(self._app_state_table)} WHERE app_name = %s", (app_name,) + ) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return None + raise + return _json_dict(_row_value(row, "state", 0)) if row is not None else None + + def get_user_state(self, app_name: str, user_id: str) -> "dict[str, Any] | None": + """Return user-scoped state.""" + try: + row = self._execute_fetchone( + f""" + SELECT TOP 1 state + FROM {_table_ref(self._user_state_table)} + WHERE app_name = %s AND user_id = %s + """, + (app_name, user_id), + ) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return None + raise + return _json_dict(_row_value(row, "state", 0)) if row is not None else None + + def upsert_app_state(self, app_name: str, state: "dict[str, Any]") -> None: + """Insert or replace app-scoped state.""" + encoded = to_json(state) + self._execute(self._upsert_app_state_sql(), (app_name, encoded, app_name, app_name, encoded), commit=True) + + def upsert_user_state(self, app_name: str, user_id: str, state: "dict[str, Any]") -> None: + """Insert or replace user-scoped state.""" + encoded = to_json(state) + self._execute( + self._upsert_user_state_sql(), + (app_name, user_id, encoded, app_name, user_id, app_name, user_id, encoded), + commit=True, + ) + + def get_metadata(self, key: str) -> "str | None": + """Return an ADK metadata value.""" + try: + row = self._execute_fetchone( + f"SELECT TOP 1 value FROM {_table_ref(self._metadata_table)} WHERE [key] = %s", (key,) + ) + except SybaseError as exc: + if _is_sybase_table_missing(exc): + return None + raise + return str(_row_value(row, "value", 0)) if row is not None else None + + def set_metadata(self, key: str, value: str) -> None: + """Set an ADK metadata value.""" + self._execute(_upsert_metadata_sql(self._metadata_table), (key, value, key, key, value), commit=True) + + def _index_specs(self) -> "list[tuple[str, str, str]]": + """Return ``(index_name, table, columns)`` specs for session and event indexes.""" + return [*_sessions_index_specs(self._session_table), *_events_index_specs(self._events_table)] + + def _sessions_table_ddl(self) -> str: + """Return Sybase ASE DDL for the ADK session table.""" + return _sessions_table_ddl(self._session_table, JSON_COLUMN_TYPE, self._owner_id_column_ddl) + + def _events_table_ddl(self) -> str: + """Return Sybase ASE DDL for the ADK event table.""" + return _events_table_ddl(self._events_table, self._session_table, JSON_COLUMN_TYPE) + + def _app_states_table_ddl(self) -> str: + """Return Sybase ASE DDL for the app-scoped state table.""" + return _app_states_table_ddl(self._app_state_table, JSON_COLUMN_TYPE) + + def _user_states_table_ddl(self) -> str: + """Return Sybase ASE DDL for the user-scoped state table.""" + return _user_states_table_ddl(self._user_state_table, JSON_COLUMN_TYPE) + + def _metadata_table_ddl(self) -> str: + """Return Sybase ASE DDL for the ADK metadata table.""" + return _metadata_table_ddl(self._metadata_table) + + def _drop_app_states_table_sql(self) -> str: + ref = escape_sql_literal(_table_ref(self._app_state_table)) + return f"IF OBJECT_ID('{ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {ref}')" + + def _drop_user_states_table_sql(self) -> str: + ref = escape_sql_literal(_table_ref(self._user_state_table)) + return f"IF OBJECT_ID('{ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {ref}')" + + def _drop_metadata_table_sql(self) -> str: + ref = escape_sql_literal(_table_ref(self._metadata_table)) + return f"IF OBJECT_ID('{ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {ref}')" + + def _drop_tables_sql(self) -> "list[str]": + events_ref = escape_sql_literal(_table_ref(self._events_table)) + sessions_ref = escape_sql_literal(_table_ref(self._session_table)) + return [ + self._drop_metadata_table_sql(), + self._drop_user_states_table_sql(), + self._drop_app_states_table_sql(), + f"IF OBJECT_ID('{events_ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {events_ref}')", + f"IF OBJECT_ID('{sessions_ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {sessions_ref}')", + ] + + def _upsert_app_state_sql(self) -> str: + return _upsert_state_sql(self._app_state_table, ("app_name",)) + + def _upsert_user_state_sql(self) -> str: + return _upsert_state_sql(self._user_state_table, ("app_name", "user_id")) + + def _events_query( + self, + app_name: str, + user_id: str, + session_id: str, + after_timestamp: "datetime | None" = None, + limit: "int | None" = None, + ) -> "tuple[str, tuple[Any, ...]]": + return _events_query(self._events_table, app_name, user_id, session_id, after_timestamp, limit) + + def _execute_fetchone(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> "Any | None": + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + cursor.execute(sql, params) + row = cursor.fetchone() + if commit: + conn.commit() + return row + + def _execute_fetchall(self, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + cursor.execute(sql, params) + return list(cursor.fetchall()) + + def _execute(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> int: + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + cursor.execute(sql, params) + rowcount = resolve_rowcount(cursor) + if commit: + conn.commit() + return rowcount + + +class SybaseADKMemoryStore(BaseSyncADKMemoryStore["SybaseConfig"]): + """Sybase ASE ADK memory store using pymssql.""" + + __slots__ = () + + def __init__(self, config: "SybaseConfig") -> None: + super().__init__(config) + + def create_tables(self) -> None: + """Create the memory table (idempotent Sybase ASE DDL) and DD-gated indexes.""" + if not self.create_schema_enabled: + self.reconcile_schema() + return + + if not self._enabled: + return + with self._config.provide_session() as driver: + driver.execute_script(self._memory_table_ddl()) + index_specs = self._memory_index_specs() + existing_indexes = _existing_index_names(driver, index_specs) + for index_name, index_table, columns in index_specs: + if _bare_name(index_name) not in existing_indexes: + driver.execute(_create_index_sql(index_table, index_name, columns)) + driver.commit() + self.reconcile_schema(assume_existing=True) + + def insert_memory_entries(self, entries: "list[StoredMemory]", owner_id: "object | None" = None) -> int: + """Bulk insert memory entries with event-id deduplication.""" + if not self._enabled: + msg = "ADK memory store is disabled" + raise RuntimeError(msg) + if not entries: + return 0 + + owner_column = f", {quote_bracket_identifier(self._owner_id_column_name)}" if self._owner_id_column_name else "" + owner_value = ", %s" if self._owner_id_column_name else "" + sql = f""" + INSERT INTO {_table_ref(self._memory_table)} ( + id, session_id, app_name, user_id, scope, event_id, author, timestamp, + content_json, content_text, metadata_json{owner_column} + ) + SELECT %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s{owner_value} + WHERE NOT EXISTS ( + SELECT 1 FROM {_table_ref(self._memory_table)} HOLDLOCK + WHERE event_id = %s + ) + """ + inserted = 0 + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + try: + for entry in entries: + params: tuple[Any, ...] = ( + entry["id"], + entry["session_id"], + entry["app_name"], + entry["user_id"], + entry.get("scope", "user"), + entry["event_id"], + entry.get("author"), + entry["timestamp"], + to_json(entry["content_json"]), + entry["content_text"], + to_json(entry["metadata_json"]) if entry.get("metadata_json") is not None else None, + ) + if self._owner_id_column_name: + params = (*params, owner_id) + cursor.execute(sql, (*params, entry["event_id"])) + inserted += resolve_rowcount(cursor) + except Exception: + conn.rollback() + raise + conn.commit() + return inserted + + def search_entries( + self, + query: str, + app_name: str, + user_id: str, + limit: "int | None" = None, + scope_filter: Literal["all", "user", "app"] = "all", + embedding: "Sequence[float] | None" = None, + ) -> "list[StoredMemory]": + """Search memory entries by text query.""" + _ = embedding + if not self._enabled: + msg = "ADK memory store is disabled" + raise RuntimeError(msg) + limit_value = limit or self._max_results + where_scope, scope_params = _build_sybase_scope_where(app_name, user_id, scope_filter) + escaped_query = _escape_like_pattern(query) + sql = f""" + SELECT + id, session_id, app_name, user_id, scope, event_id, author, timestamp, + content_json, content_text, metadata_json, inserted_at + FROM {_table_ref(self._memory_table)} + WHERE {where_scope} AND content_text LIKE %s ESCAPE '\\' + ORDER BY timestamp DESC + ROWS LIMIT %s + """ + rows = self._execute_fetchall(sql, (*scope_params, f"%{escaped_query}%", limit_value)) + return [_memory_record_from_row(row) for row in rows] + + def delete_entries_by_session(self, session_id: str) -> int: + """Delete all memory entries for a specific session.""" + return self._execute( + f"DELETE FROM {_table_ref(self._memory_table)} WHERE session_id = %s", (session_id,), commit=True + ) + + def delete_entries_older_than(self, days: int, app_name: "str | None" = None, scope: "str | None" = None) -> int: + """Delete memory entries older than the retention window.""" + clauses = ["inserted_at < DATEADD(day, -%s, GETUTCDATE())"] + params: list[Any] = [days] + if app_name is not None: + clauses.append("app_name = %s") + params.append(app_name) + if scope is not None: + clauses.append("scope = %s") + params.append(scope) + where_sql = " AND ".join(clauses) + return self._execute( + f"DELETE FROM {_table_ref(self._memory_table)} WHERE {where_sql}", tuple(params), commit=True + ) + + def _memory_table_ddl(self) -> str: + owner_line = f", {self._owner_id_column_ddl}" if self._owner_id_column_ddl else "" + table_ref = _table_ref(self._memory_table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "session_id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + f"scope VARCHAR(16) DEFAULT 'user' NOT NULL, " + "event_id VARCHAR(128) NOT NULL, " + "author VARCHAR(256) NULL, " + "timestamp BIGDATETIME NOT NULL, " + f"content_json {JSON_COLUMN_TYPE} NOT NULL, " + "content_text TEXT NOT NULL, " + f"metadata_json {JSON_COLUMN_TYPE} NULL, " + f"inserted_at BIGDATETIME DEFAULT GETUTCDATE() NOT NULL{owner_line}, " + f"CONSTRAINT {_constraint_ref('pk', self._memory_table, 'id')} PRIMARY KEY (id), " + f"CONSTRAINT {_constraint_ref('uq', self._memory_table, 'event_id')} UNIQUE (event_id)" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + def _memory_index_specs(self) -> "list[tuple[str, str, str]]": + """Return ``(index_name, table, columns)`` specs for memory-table indexes.""" + _, table_name = split_schema_and_table(self._memory_table) + return [ + (f"idx_{table_name}_app_scope_user_time", self._memory_table, "app_name, scope, user_id, timestamp DESC"), + (f"idx_{table_name}_scope", self._memory_table, "app_name, scope"), + (f"idx_{table_name}_session", self._memory_table, "session_id"), + (f"idx_{table_name}_timestamp", self._memory_table, "timestamp DESC"), + ] + + def _drop_memory_table_sql(self) -> "list[str]": + ref = escape_sql_literal(_table_ref(self._memory_table)) + return [f"IF OBJECT_ID('{ref}', 'U') IS NOT NULL EXECUTE('DROP TABLE {ref}')"] + + def _execute_fetchall(self, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + cursor.execute(sql, params) + return list(cursor.fetchall()) + + def _execute(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> int: + with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: + cursor.execute(sql, params) + rowcount = resolve_rowcount(cursor) + if commit: + conn.commit() + return rowcount + + +def _existing_index_names(driver: "SybaseDriver", index_specs: "list[tuple[str, str, str]]") -> "set[str]": + schemas = {split_schema_and_table(index_table)[0] for _, index_table, _ in index_specs} or {SYBASE_SCHEMA} + existing: set[str] = set() + for schema_name in schemas: + existing.update(_casefold_names(driver.data_dictionary.get_indexes(driver, schema=schema_name), "index_name")) + return existing + + +def _sessions_table_ddl(table: str, json_column_type: str, owner_id_column_ddl: "str | None") -> str: + owner_line = f", {owner_id_column_ddl}" if owner_id_column_ddl else "" + table_ref = _table_ref(table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + f"user_id VARCHAR(128) NOT NULL{owner_line}, " + f"state {json_column_type} NOT NULL, " + "create_time BIGDATETIME DEFAULT GETUTCDATE() NOT NULL, " + "update_time BIGDATETIME DEFAULT GETUTCDATE() NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'id')} PRIMARY KEY (id)" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + +def _sessions_index_specs(table: str) -> "list[tuple[str, str, str]]": + _, table_name = split_schema_and_table(table) + return [ + (f"idx_{table_name}_app_user", table, "app_name, user_id"), + (f"idx_{table_name}_update_time", table, "update_time DESC"), + ] + + +def _events_table_ddl(table: str, session_table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + session_ref = _table_ref(session_table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + "session_id VARCHAR(128) NOT NULL, " + "invocation_id VARCHAR(256) NOT NULL, " + "timestamp BIGDATETIME NOT NULL, " + f"event_data {json_column_type} NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'id')} PRIMARY KEY (id), " + f"CONSTRAINT {_constraint_ref('fk', table, 'session')} FOREIGN KEY (session_id) REFERENCES {session_ref}(id)" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + +def _events_index_specs(table: str) -> "list[tuple[str, str, str]]": + _, table_name = split_schema_and_table(table) + return [ + (f"idx_{table_name}_scope", table, "app_name, user_id, session_id, timestamp ASC"), + (f"idx_{table_name}_session", table, "session_id, timestamp ASC"), + (f"idx_{table_name}_invocation", table, "invocation_id"), + (f"idx_{table_name}_timestamp", table, "timestamp ASC"), + (f"idx_{table_name}_app_timestamp", table, "app_name, timestamp ASC"), + ] + + +def _app_states_table_ddl(table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "app_name VARCHAR(128) NOT NULL, " + f"state {json_column_type} NOT NULL, " + "update_time BIGDATETIME DEFAULT GETUTCDATE() NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'app_name')} PRIMARY KEY (app_name)" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + +def _user_states_table_ddl(table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + f"state {json_column_type} NOT NULL, " + "update_time BIGDATETIME DEFAULT GETUTCDATE() NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'app_user')} PRIMARY KEY (app_name, user_id)" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + +def _metadata_table_ddl(table: str) -> str: + table_ref = _table_ref(table) + escaped_ref = escape_sql_literal(table_ref) + create_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "[key] VARCHAR(128) NOT NULL, " + "value VARCHAR(512) NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'key')} PRIMARY KEY ([key])" + ")" + ) + return f"IF OBJECT_ID('{escaped_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + +def _create_index_sql(table: str, index_name: str, columns: str) -> str: + return f"CREATE INDEX {quote_bracket_identifier(index_name)} ON {_table_ref(table)} ({columns})" + + +def _casefold_names(rows: "list[Any]", key: str) -> "set[str]": + """Collapse data-dictionary rows into a case-folded, schema-stripped name set.""" + return {str(row.get(key, "")).rsplit(".", 1)[-1].casefold() for row in rows} + + +def _bare_name(name: str) -> str: + """Return the case-folded, schema-stripped object name for membership checks.""" + return name.rsplit(".", 1)[-1].casefold() + + +def _insert_event_sql(table: str) -> str: + return f""" + INSERT INTO {_table_ref(table)} ( + id, app_name, user_id, session_id, invocation_id, timestamp, event_data + ) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """ + + +def _upsert_state_sql(table: str, key_columns: "tuple[str, ...]") -> str: + table_ref = _table_ref(table) + where_clause = " AND ".join(f"{quote_bracket_identifier(col)} = %s" for col in key_columns) + insert_columns = ", ".join(quote_bracket_identifier(col) for col in (*key_columns, "state", "update_time")) + insert_placeholders = ", ".join(["%s" for _ in key_columns] + ["%s", "GETUTCDATE()"]) + return f""" + IF EXISTS (SELECT 1 FROM {table_ref} HOLDLOCK WHERE {where_clause}) + UPDATE {table_ref} SET state = %s, update_time = GETUTCDATE() WHERE {where_clause} + ELSE + INSERT INTO {table_ref} ({insert_columns}) VALUES ({insert_placeholders}) + """ + + +def _upsert_metadata_sql(table: str) -> str: + table_ref = _table_ref(table) + return f""" + IF EXISTS (SELECT 1 FROM {table_ref} HOLDLOCK WHERE [key] = %s) + UPDATE {table_ref} SET value = %s WHERE [key] = %s + ELSE + INSERT INTO {table_ref} ([key], value) VALUES (%s, %s) + """ + + +def _events_query( + table: str, app_name: str, user_id: str, session_id: str, after_timestamp: "datetime | None", limit: "int | None" +) -> "tuple[str, tuple[Any, ...]]": + params: list[Any] = [app_name, user_id, session_id] + after_clause = "" + if after_timestamp is not None: + after_clause = " AND timestamp > %s" + params.append(after_timestamp) + limit_clause = "" + if limit is not None: + limit_clause = "\n ROWS LIMIT %s" + params.append(limit) + sql = f""" + SELECT id, app_name, user_id, session_id, invocation_id, timestamp, event_data + FROM {_table_ref(table)} + WHERE app_name = %s AND user_id = %s AND session_id = %s{after_clause} + ORDER BY timestamp ASC{limit_clause} + """ + return sql, tuple(params) + + +def _event_insert_params(event_record: StoredEvent) -> "tuple[Any, ...]": + return ( + event_record["id"], + event_record["app_name"], + event_record["user_id"], + event_record["session_id"], + event_record["invocation_id"], + event_record["timestamp"], + to_json(event_record["event_data"]), + ) + + +def _row_value(row: object, key: str, index: int) -> Any: + if isinstance(row, dict): + if key in row: + return row[key] + upper_key = key.upper() + if upper_key in row: + return row[upper_key] + return None + if isinstance(row, (list, tuple)) and len(row) > index: + return row[index] + return getattr(row, key, None) + + +def _session_record_from_row(row: Any) -> StoredSession: + return StoredSession( + id=_row_value(row, "id", 0), + app_name=_row_value(row, "app_name", 1), + user_id=_row_value(row, "user_id", 2), + state=_json_dict(_row_value(row, "state", 3)), + create_time=_row_value(row, "create_time", 4), + update_time=_row_value(row, "update_time", 5), + ) + + +def _event_record_from_row(row: Any) -> StoredEvent: + return StoredEvent( + id=_row_value(row, "id", 0), + app_name=_row_value(row, "app_name", 1), + user_id=_row_value(row, "user_id", 2), + session_id=_row_value(row, "session_id", 3), + invocation_id=_row_value(row, "invocation_id", 4), + timestamp=_row_value(row, "timestamp", 5), + event_data=_json_dict(_row_value(row, "event_data", 6)), + ) + + +def _memory_record_from_row(row: Any) -> "StoredMemory": + metadata_raw = _row_value(row, "metadata_json", 10) + return cast( + "StoredMemory", + { + "id": _row_value(row, "id", 0), + "session_id": _row_value(row, "session_id", 1), + "app_name": _row_value(row, "app_name", 2), + "user_id": _row_value(row, "user_id", 3), + "scope": _row_value(row, "scope", 4), + "event_id": _row_value(row, "event_id", 5), + "author": _row_value(row, "author", 6), + "timestamp": _row_value(row, "timestamp", 7), + "content_json": _json_dict(_row_value(row, "content_json", 8)), + "content_text": _row_value(row, "content_text", 9), + "metadata_json": _json_dict(metadata_raw) if metadata_raw is not None else None, + "inserted_at": _row_value(row, "inserted_at", 11), + "embedding": None, + }, + ) + + +def _json_dict(value: Any) -> "dict[str, Any]": + if value is None: + return {} + if isinstance(value, dict): + return cast("dict[str, Any]", value) + if isinstance(value, bytearray): + value = bytes(value) + if isinstance(value, (bytes, str)): + return cast("dict[str, Any]", from_json(value)) + return cast("dict[str, Any]", from_json(str(value))) + + +def _is_sybase_table_missing(exc: BaseException) -> bool: + text = str(exc).lower() + return ( + "not found" in text + or "invalid object name" in text + or extract_error_number(exc) == SYBASE_TABLE_NOT_FOUND_ERROR + ) + + +def _table_ref(table: str) -> str: + schema_name, table_name = split_schema_and_table(table) + return f"{quote_bracket_identifier(schema_name)}.{quote_bracket_identifier(table_name)}" + + +def _constraint_ref(prefix: str, table: str, suffix: str) -> str: + _, table_name = split_schema_and_table(table) + return quote_bracket_identifier(f"{prefix}_{table_name}_{suffix}") + + +def _raise_session_not_found(session_id: str) -> None: + msg = f"Session {session_id} not found during append_event_and_update_state." + raise ValueError(msg) + + +def _build_sybase_scope_where( + app_name: str, user_id: str, scope_filter: Literal["all", "user", "app"] +) -> tuple[str, tuple[Any, ...]]: + if scope_filter == "all": + return "app_name = %s AND ((scope = 'user' AND user_id = %s) OR scope = 'app')", (app_name, user_id) + if scope_filter == "user": + return "app_name = %s AND scope = 'user' AND user_id = %s", (app_name, user_id) + return "app_name = %s AND scope = 'app'", (app_name,) + + +def _escape_like_pattern(value: str) -> str: + return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_").replace("[", "\\[") + + +def _session_list_query( + session_table: str, + app_name: str, + user_id: "str | None", + column: str, + direction: str, + limit: "int | None", + offset: int, +) -> "tuple[str, tuple[Any, ...]]": + """Return the bounded Sybase ASE session-list query and its bound values.""" + params: list[Any] = [app_name] + where_clause = "app_name = %s" + if user_id is not None: + params.append(user_id) + where_clause = f"{where_clause} AND user_id = %s" + + page_clause = "" + if limit is not None: + params.extend((limit, offset)) + page_clause = "\n ROWS LIMIT %s OFFSET %s" + + sql = f""" + SELECT id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(session_table)} + WHERE {where_clause} + ORDER BY {column} {direction}, id {direction}{page_clause} + """ + return sql, tuple(params) diff --git a/sqlspec/adapters/sybase/config.py b/sqlspec/adapters/sybase/config.py new file mode 100644 index 000000000..67816299c --- /dev/null +++ b/sqlspec/adapters/sybase/config.py @@ -0,0 +1,183 @@ +"""Sybase ASE database configuration.""" + +from collections.abc import Callable, Mapping +from typing import TYPE_CHECKING, Any, ClassVar, Literal, NotRequired, TypedDict, cast + +from sqlspec.adapters.sybase._typing import SybaseConnection, SybaseCursor, SybaseRawCursor, SybaseSessionContext +from sqlspec.adapters.sybase.core import apply_driver_features, build_connection_config, default_statement_config +from sqlspec.adapters.sybase.driver import SybaseDriver, SybaseExceptionHandler +from sqlspec.adapters.sybase.migrations import SybaseSyncMigrationTracker +from sqlspec.adapters.sybase.pool import SybaseConnectionPool +from sqlspec.config import ExtensionConfigs, SyncDatabaseConfig +from sqlspec.core import TypeCoercionCapabilities +from sqlspec.driver import SyncPoolConnectionContext, SyncPoolSessionFactory +from sqlspec.extensions.events import EventRuntimeHints +from sqlspec.utils.config_tools import normalize_connection_config + +if TYPE_CHECKING: + from sqlspec.core import StatementConfig + from sqlspec.observability import ObservabilityConfig + +__all__ = ( + "SybaseConfig", + "SybaseConnectionContext", + "SybaseConnectionParams", + "SybaseDriverFeatures", + "SybasePoolParams", + "SybaseTimeout", +) + +SybaseTimeout = int | float + + +class SybaseConnectionParams(TypedDict): + """Sybase ASE connection parameters.""" + + server: NotRequired[str] + host: NotRequired[str] + user: NotRequired[str] + username: NotRequired[str] + password: NotRequired[str] + database: NotRequired[str] + db: NotRequired[str] + port: NotRequired[int | str] + timeout: NotRequired[SybaseTimeout] + login_timeout: NotRequired[SybaseTimeout] + charset: NotRequired[str] + as_dict: NotRequired[bool] + appname: NotRequired[str] + conn_properties: NotRequired[str] + autocommit: NotRequired[bool] + tds_version: NotRequired[str] + encryption: NotRequired[Literal["off", "request", "require"]] + arraysize: NotRequired[int] + conv: NotRequired[Mapping[int | type[Any], Callable[..., Any]]] + read_only: NotRequired[bool] + pool_recycle_seconds: NotRequired[int] + health_check_interval: NotRequired[float] + extra: NotRequired["dict[str, Any]"] + + +class SybasePoolParams(SybaseConnectionParams): + """Sybase ASE pool parameters.""" + + +class SybaseDriverFeatures(TypedDict): + """Sybase ASE driver feature flags.""" + + json_serializer: NotRequired["Callable[[Any], str]"] + json_deserializer: NotRequired["Callable[[str], Any]"] + on_connection_create: "NotRequired[Callable[[SybaseConnection], None]]" + enable_events: NotRequired[bool] + events_backend: NotRequired[Literal["poll_queue"]] + + +class SybaseConnectionContext(SyncPoolConnectionContext): + """Context manager for Sybase ASE connections.""" + + __slots__ = () + + +class _SybaseSessionConnectionHandler(SyncPoolSessionFactory): + __slots__ = () + + +class SybaseConfig(SyncDatabaseConfig[SybaseConnection, SybaseConnectionPool, SybaseDriver]): + """Configuration for Sybase ASE synchronous connections over pymssql (FreeTDS TDS 5.0).""" + + __slots__ = ("_user_connection_hook",) + + driver_type: "ClassVar[type[SybaseDriver]]" = SybaseDriver + connection_type: "ClassVar[type[SybaseConnection]]" = cast("type[SybaseConnection]", SybaseConnection) + migration_tracker_type: "ClassVar[type[SybaseSyncMigrationTracker]]" = SybaseSyncMigrationTracker + supports_transactional_ddl: "ClassVar[bool]" = False + supports_migration_schemas: "ClassVar[bool]" = False + supports_native_arrow_export: "ClassVar[bool]" = True + supports_native_arrow_import: "ClassVar[bool]" = True + supports_native_parquet_export: "ClassVar[bool]" = True + supports_native_parquet_import: "ClassVar[bool]" = True + supports_native_row_streaming: "ClassVar[bool]" = True + type_coercion_capabilities: "ClassVar[TypeCoercionCapabilities]" = TypeCoercionCapabilities( + datetime_binding="native", timestamp_precision="microsecond", json_columns_decoded=False, uuid_binding="text" + ) + _connection_context_class: "ClassVar[type[SybaseConnectionContext]]" = SybaseConnectionContext + _session_factory_class: "ClassVar[type[_SybaseSessionConnectionHandler]]" = _SybaseSessionConnectionHandler + _session_context_class: "ClassVar[type[SybaseSessionContext]]" = SybaseSessionContext + _default_statement_config = default_statement_config + + def __init__( + self, + *, + connection_config: "SybasePoolParams | dict[str, Any] | None" = None, + connection_instance: "SybaseConnectionPool | None" = None, + migration_config: "dict[str, Any] | None" = None, + statement_config: "StatementConfig | None" = None, + driver_features: "SybaseDriverFeatures | dict[str, Any] | None" = None, + bind_key: "str | None" = None, + extension_config: "ExtensionConfigs | None" = None, + observability_config: "ObservabilityConfig | None" = None, + **kwargs: Any, + ) -> None: + connection_config = build_connection_config(normalize_connection_config(connection_config)) + + statement_config = statement_config or default_statement_config + statement_config, driver_features = apply_driver_features(statement_config, driver_features) + + features_dict = dict(driver_features) if driver_features else {} + self._user_connection_hook: Callable[[SybaseConnection], None] | None = features_dict.pop( + "on_connection_create", None + ) + + super().__init__( + connection_config=connection_config, + connection_instance=connection_instance, + migration_config=migration_config, + statement_config=statement_config, + driver_features=features_dict, + bind_key=bind_key, + extension_config=extension_config, + observability_config=observability_config, + **kwargs, + ) + + def create_connection(self) -> "SybaseConnection": + """Open a standalone Sybase ASE connection owned by the caller.""" + return self.provide_pool().new_connection() + + def get_signature_namespace(self) -> "dict[str, Any]": + namespace = super().get_signature_namespace() + namespace.update({ + "SybaseConfig": SybaseConfig, + "SybaseConnection": SybaseConnection, + "SybaseConnectionContext": SybaseConnectionContext, + "SybaseConnectionParams": SybaseConnectionParams, + "SybaseConnectionPool": SybaseConnectionPool, + "SybaseCursor": SybaseCursor, + "SybaseDriver": SybaseDriver, + "SybaseDriverFeatures": SybaseDriverFeatures, + "SybaseExceptionHandler": SybaseExceptionHandler, + "SybasePoolParams": SybasePoolParams, + "SybaseRawCursor": SybaseRawCursor, + "SybaseSessionContext": SybaseSessionContext, + }) + return namespace + + def get_event_runtime_hints(self) -> "EventRuntimeHints": + """Return runtime hints for Sybase ASE event channels.""" + return EventRuntimeHints(poll_interval=0.25, lease_seconds=5) + + def _create_pool(self) -> "SybaseConnectionPool": + config = dict(self.connection_config) + pool_recycle = config.pop("pool_recycle_seconds", 86400) + health_check = config.pop("health_check_interval", 30.0) + return SybaseConnectionPool( + config, + recycle_seconds=pool_recycle, + health_check_interval=health_check, + on_connection_create=self._user_connection_hook, + ) + + def _close_pool(self) -> None: + if self.connection_instance: + self.connection_instance.close() + self.connection_instance = None diff --git a/sqlspec/adapters/sybase/core.py b/sqlspec/adapters/sybase/core.py new file mode 100644 index 000000000..37d946fcf --- /dev/null +++ b/sqlspec/adapters/sybase/core.py @@ -0,0 +1,411 @@ +"""Sybase ASE adapter compiled helpers.""" + +import re +from collections.abc import Callable, Mapping, Sized +from typing import TYPE_CHECKING, Any, Final, Literal + +from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile +from sqlspec.dialects.sybase import Sybase as _SybaseDialect +from sqlspec.exceptions import ( + CheckViolationError, + DatabaseConnectionError, + DataError, + DeadlockError, + ForeignKeyViolationError, + IntegrityError, + NotFoundError, + NotNullViolationError, + OperationalError, + PermissionDeniedError, + QueryTimeoutError, + SQLParsingError, + SQLSpecError, + UniqueViolationError, +) +from sqlspec.utils.serializers import from_json, to_json +from sqlspec.utils.text import split_qualified_identifier +from sqlspec.utils.type_converters import build_uuid_coercions +from sqlspec.utils.type_guards import has_rowcount + +if TYPE_CHECKING: + from collections.abc import Sequence + from logging import Logger + + from sqlspec.core.parameters import ParameterValidator + +__all__ = ( + "DEFAULT_SYBASE_CONN_PROPERTIES", + "DEFAULT_SYBASE_TDS_VERSION", + "apply_driver_features", + "build_connection_config", + "build_insert_statement", + "build_profile", + "build_statement_config", + "collect_rows", + "create_mapped_exception", + "default_statement_config", + "driver_profile", + "escape_literal_percent", + "escape_sql_literal", + "extract_error_number", + "format_identifier", + "normalize_execute_many_parameters", + "normalize_execute_parameters", + "quote_bracket_identifier", + "resolve_column_names", + "resolve_many_rowcount", + "resolve_rowcount", + "split_schema_and_table", +) + +DEFAULT_SYBASE_CONN_PROPERTIES: Final[str] = "SET QUOTED_IDENTIFIER ON; SET ANSINULL ON; SET TEXTSIZE 2147483647;" +DEFAULT_SYBASE_TDS_VERSION: Final[str] = "5.0" + +_ERROR_NUMBER_PATTERN: Final[re.Pattern[str]] = re.compile(r"(?:\(([-]?\d+)(?:,|\))|\bMsg\s+([-]?\d+)\b)") +_SYBASE_CONSTRAINT_547: Final[int] = 547 +_COLUMN_CACHE_MAX_SIZE: Final[int] = 256 +_ERROR_CODE_MAPPING: Final[dict[int, tuple[type[SQLSpecError], str]]] = { + 2601: (UniqueViolationError, "unique index violation"), + 2627: (UniqueViolationError, "unique constraint violation"), + 546: (ForeignKeyViolationError, "foreign key constraint violation"), + 548: (CheckViolationError, "check constraint violation"), + 233: (NotNullViolationError, "not-null constraint violation"), + 515: (NotNullViolationError, "not-null constraint violation"), + 1205: (DeadlockError, "deadlock detected"), + 12205: (QueryTimeoutError, "lock wait period exceeded"), + -2: (QueryTimeoutError, "query timeout"), + 1222: (QueryTimeoutError, "lock request time out period exceeded"), + 102: (SQLParsingError, "SQL syntax error"), + 105: (SQLParsingError, "unclosed quotation mark"), + 156: (SQLParsingError, "incorrect syntax near keyword"), + 11021: (SQLParsingError, "invalid function invocation"), + 207: (NotFoundError, "invalid column name"), + 208: (NotFoundError, "object not found"), + 4002: (DatabaseConnectionError, "login failed"), + 4060: (DatabaseConnectionError, "cannot open database"), + 18456: (DatabaseConnectionError, "login failed"), + 20002: (DatabaseConnectionError, "Adaptive Server connection failed"), + 20009: (DatabaseConnectionError, "Adaptive Server is unavailable or does not exist"), + 1105: (OperationalError, "cannot allocate space in database"), + 2762: (OperationalError, "DDL command is not allowed within a multi-statement transaction"), + 20047: (OperationalError, "DBPROCESS is dead or not enabled"), + 168: (DataError, "numeric scale conversion error"), + 220: (DataError, "arithmetic overflow error"), + 241: (DataError, "datetime conversion error"), + 242: (DataError, "datetime conversion out of range"), + 245: (DataError, "data conversion error"), + 247: (DataError, "arithmetic overflow during conversion"), + 3606: (DataError, "arithmetic overflow occurred"), + 9502: (DataError, "character or binary data would be truncated"), + 229: (PermissionDeniedError, "permission denied"), + 230: (PermissionDeniedError, "permission denied"), + 262: (PermissionDeniedError, "permission denied"), + 10330: (PermissionDeniedError, "object permission denied"), + 10331: (PermissionDeniedError, "database permission denied"), + 10332: (PermissionDeniedError, "execute permission denied"), +} + + +def quote_bracket_identifier(identifier: str) -> str: + """Quote a Sybase T-SQL identifier with square brackets.""" + cleaned = identifier.strip() + if (cleaned.startswith("[") and cleaned.endswith("]")) or (cleaned.startswith('"') and cleaned.endswith('"')): + cleaned = cleaned[1:-1].replace("]]", "]").replace('""', '"') + return f"[{cleaned.replace(']', ']]')}]" + + +def escape_sql_literal(value: str) -> str: + """Escape single quotes inside a SQL string literal.""" + return value.replace("'", "''") + + +def split_schema_and_table(identifier: str, *, default_schema: str = "dbo") -> tuple[str, str]: + """Split a table reference into unquoted ``(schema, table)`` names.""" + cleaned = identifier.strip() + if not cleaned: + msg = "Table name must not be empty" + raise SQLSpecError(msg) + parts = split_qualified_identifier(cleaned, quote_chars='"', allow_bracket_quotes=True) + if not parts: + return default_schema, cleaned + if len(parts) == 1: + return default_schema, parts[0] + return parts[-2], parts[-1] + + +def format_identifier(identifier: str) -> str: + """Format a Sybase T-SQL identifier with bracket quoting.""" + cleaned = identifier.strip() + if not cleaned: + msg = "Table name must not be empty" + raise SQLSpecError(msg) + parts = split_qualified_identifier(cleaned, quote_chars='"', allow_bracket_quotes=True) + return ".".join(quote_bracket_identifier(part) for part in parts) + + +def build_insert_statement(table: str, columns: "list[str]") -> str: + """Build a pymssql-compatible INSERT statement for Sybase ASE.""" + column_clause = ", ".join(quote_bracket_identifier(column) for column in columns) + placeholders = ", ".join("%s" for _ in columns) + return f"INSERT INTO {format_identifier(table)} ({column_clause}) VALUES ({placeholders})" + + +def escape_literal_percent(sql: str, parameters: Any, validator: "ParameterValidator") -> str: + """Escape literal percent characters before driver-side interpolation.""" + if not parameters or "%" not in sql: + return sql + keep = {info.position for info in validator.extract_parameters(sql)} + return "".join("%%" if char == "%" and index not in keep else char for index, char in enumerate(sql)) + + +def normalize_execute_parameters(parameters: Any) -> Any: + """Normalize parameters for Sybase pymssql execute calls.""" + if parameters is None: + return None + if isinstance(parameters, list): + return tuple(parameters) + return parameters + + +def normalize_execute_many_parameters(parameters: Any) -> Any: + """Normalize parameters for Sybase pymssql executemany calls.""" + return parameters + + +def build_profile() -> "DriverParameterProfile": + """Create the Sybase ASE driver parameter profile.""" + return DriverParameterProfile( + name="sybase", + default_style=ParameterStyle.QMARK, + supported_styles={ParameterStyle.QMARK, ParameterStyle.NAMED_PYFORMAT}, + default_execution_style=ParameterStyle.POSITIONAL_PYFORMAT, + supported_execution_styles={ParameterStyle.POSITIONAL_PYFORMAT}, + has_native_list_expansion=False, + preserve_parameter_format=True, + needs_static_script_compilation=False, + allow_mixed_parameter_styles=False, + preserve_original_params_for_many=False, + json_serializer_strategy="helper", + custom_type_coercions=_custom_type_coercions(), + default_dialect="sybase", + ) + + +def build_statement_config( + *, json_serializer: "Callable[[Any], str] | None" = None, json_deserializer: "Callable[[str], Any] | None" = None +) -> "StatementConfig": + """Construct the Sybase ASE statement configuration.""" + return build_statement_config_from_profile( + driver_profile, + statement_overrides={"dialect": "sybase"}, + json_serializer=json_serializer or to_json, + json_deserializer=json_deserializer or from_json, + ) + + +def apply_driver_features( + statement_config: "StatementConfig", driver_features: "Mapping[str, Any] | None" +) -> "tuple[StatementConfig, dict[str, Any]]": + """Apply Sybase ASE driver feature defaults to statement config.""" + features: dict[str, Any] = dict(driver_features) if driver_features else {} + json_serializer = features.setdefault("json_serializer", to_json) + json_deserializer = features.setdefault("json_deserializer", from_json) + + if json_serializer is not None and ( + json_serializer is not to_json + or json_deserializer is not from_json + or statement_config.parameter_config.json_serializer is not to_json + ): + parameter_config = statement_config.parameter_config.with_json_serializers( + json_serializer, deserializer=json_deserializer + ) + statement_config = statement_config.replace(parameter_config=parameter_config) + + return statement_config, features + + +def create_mapped_exception(error: BaseException, *, logger: "Logger | None" = None) -> SQLSpecError: + """Map a Sybase ASE exception to SQLSpec's exception hierarchy.""" + error_number = extract_error_number(error) + + if error_number == _SYBASE_CONSTRAINT_547: + message = str(error) + if "check constraint" in message.lower(): + return CheckViolationError(f"Sybase ASE error 547: check constraint violation. Original error: {error}") + return ForeignKeyViolationError( + f"Sybase ASE error 547: foreign key constraint violation. Original error: {error}" + ) + if error_number is not None: + mapping = _ERROR_CODE_MAPPING.get(error_number) + if mapping is not None: + error_class, description = mapping + return error_class(f"Sybase ASE error {error_number}: {description}. Original error: {error}") + if logger is not None: + logger.debug("Unmapped Sybase ASE error number: %s", error_number) + + constraint_exception = _constraint_exception_from_message(error) + if constraint_exception is not None: + return constraint_exception + + exc_name = type(error).__name__ + if exc_name == "IntegrityError": + return IntegrityError(f"Sybase ASE integrity error. Original error: {error}") + if exc_name == "OperationalError": + return OperationalError(f"Sybase ASE operational error. Original error: {error}") + if exc_name == "DataError": + return DataError(f"Sybase ASE data error. Original error: {error}") + return SQLSpecError(f"Sybase ASE database error. Original error: {error}") + + +def resolve_column_names( + description: "Sequence[Any] | None", column_name_cache: "dict[int, tuple[Any, list[str]]] | None" = None +) -> "list[str]": + """Resolve ordered column names from cursor metadata.""" + if not description: + return [] + if column_name_cache is None: + return [desc[0] for desc in description] + cache_key = id(description) + cached = column_name_cache.get(cache_key) + if cached is not None and cached[0] is description: + return cached[1] + column_names = [desc[0] for desc in description] + if len(column_name_cache) >= _COLUMN_CACHE_MAX_SIZE: + column_name_cache.pop(next(iter(column_name_cache))) + column_name_cache[cache_key] = (description, column_names) + return column_names + + +def collect_rows( + fetched_data: "Sequence[Any] | None", + description: "Sequence[Any] | None", + column_name_cache: "dict[int, tuple[Any, list[str]]] | None" = None, +) -> "tuple[list[Any], list[str], Literal['dict', 'tuple', 'record']]": + """Collect Sybase pymssql rows, preserving dictionary or tuple row shape.""" + column_names = resolve_column_names(description, column_name_cache) + if not fetched_data: + return [], column_names, "tuple" + rows = fetched_data if isinstance(fetched_data, list) else list(fetched_data) + if isinstance(rows[0], dict): + return rows, column_names, "dict" + return rows, column_names, "tuple" + + +def resolve_rowcount(cursor: Any) -> int: + """Resolve rowcount from a Sybase pymssql cursor.""" + if not has_rowcount(cursor): + return 0 + rowcount = cursor.rowcount + if isinstance(rowcount, int) and rowcount >= 0: + return rowcount + return 0 + + +def resolve_many_rowcount(cursor: Any, parameters: Any, *, fallback_count: "int | None" = None) -> int: + """Resolve executemany rowcount using cursor metadata with payload fallback.""" + rowcount = resolve_rowcount(cursor) + if rowcount > 0: + return rowcount + if fallback_count is not None: + return fallback_count + if isinstance(parameters, Sized): + return len(parameters) + return 0 + + +def extract_error_number(exc: BaseException | None) -> int | None: + """Extract integer Sybase ASE error code from an exception if present. + + Checks native integer attributes (number, error_code, errno) or args[0] before regex. + """ + if exc is None: + return None + for attr in ("number", "error_code", "errno"): + val = getattr(exc, attr, None) + if isinstance(val, int) and not isinstance(val, bool) and val != 0: + return val + if exc.args: + first = exc.args[0] + if isinstance(first, int) and not isinstance(first, bool) and first != 0: + return first + if isinstance(first, tuple) and first: + nested = first[0] + if isinstance(nested, int) and not isinstance(nested, bool) and nested != 0: + return nested + matches = _ERROR_NUMBER_PATTERN.findall(str(exc)) + if not matches: + return None + last_match = matches[-1] + return int(last_match[0] or last_match[1]) + + +def build_connection_config(connection_config: "Mapping[str, Any]") -> "dict[str, Any]": + """Build a normalized Sybase ASE connection configuration dictionary. + + Injects ``DEFAULT_SYBASE_CONN_PROPERTIES``, ``autocommit=True``, ``port=5000``, + and ``charset='UTF-8'`` by default, and strips ``tds_version`` into + ``_sybase_tds_version`` so ``SybaseConnectionPool`` can negotiate TDS 5.0 + via ``os.environ['TDSVER']`` without triggering ``pymssql._mssql``'s Cython + ``tds_version`` validator. + + Args: + connection_config: Raw connection configuration mapping. + + Returns: + Normalized connection parameters. + """ + config = dict(connection_config) + extra = config.pop("extra", None) + if isinstance(extra, Mapping): + for extra_key, extra_value in extra.items(): + if extra_value is not None: + config.setdefault(str(extra_key), extra_value) + config.setdefault("server", config.pop("host", "localhost")) + config.setdefault("port", 5000) + config.setdefault("charset", "UTF-8") + config.setdefault("autocommit", True) + config.setdefault("conn_properties", DEFAULT_SYBASE_CONN_PROPERTIES) + db_alias = config.pop("db", None) + if "database" not in config and db_alias is not None: + config["database"] = db_alias + user_alias = config.pop("username", None) + if "user" not in config and user_alias is not None: + config["user"] = user_alias + tds_version = str(config.pop("tds_version", config.pop("_sybase_tds_version", DEFAULT_SYBASE_TDS_VERSION))) + config["_sybase_tds_version"] = tds_version + return config + + +def _bool_to_int(value: bool) -> int: + return int(value) + + +def _constraint_exception_from_message(error: BaseException) -> "SQLSpecError | None": + """Classify Sybase ASE constraint messages when a driver omits the native error number.""" + message = str(error) + normalized = message.lower() + if "unique key constraint" in normalized or "duplicate key" in normalized: + return UniqueViolationError(f"Sybase ASE unique constraint violation. Original error: {message}") + if ( + "cannot insert the value null" in normalized + or "does not allow nulls" in normalized + or "does not allow null values" in normalized + ): + return NotNullViolationError(f"Sybase ASE not-null constraint violation. Original error: {message}") + if "check constraint" in normalized: + return CheckViolationError(f"Sybase ASE check constraint violation. Original error: {message}") + if "foreign key constraint" in normalized: + return ForeignKeyViolationError(f"Sybase ASE foreign key constraint violation. Original error: {message}") + return None + + +def _custom_type_coercions() -> dict[type, Callable[[Any], Any]]: + """Return custom type coercions for Sybase ASE.""" + coercions: dict[type, Callable[[Any], Any]] = {bool: _bool_to_int} + coercions.update(build_uuid_coercions()) + return coercions + + +_ = _SybaseDialect +driver_profile = build_profile() +default_statement_config = build_statement_config() diff --git a/sqlspec/adapters/sybase/data_dictionary.py b/sqlspec/adapters/sybase/data_dictionary.py new file mode 100644 index 000000000..8492b2d8e --- /dev/null +++ b/sqlspec/adapters/sybase/data_dictionary.py @@ -0,0 +1,275 @@ +"""Sybase ASE database data dictionary implementation.""" + +import logging +from typing import TYPE_CHECKING, Any, ClassVar, cast + +from mypy_extensions import mypyc_attr + +from sqlspec.data_dictionary import ( + ColumnMetadata, + ForeignKeyMetadata, + IndexMetadata, + MetadataCapabilityProfile, + MetadataResult, + TableMetadata, +) +from sqlspec.data_dictionary.dialects.sybase import ( + SYBASE_CONFIG, + SYBASE_VERSION_PATTERN, + SybaseVersionInfo, + build_sybase_metadata_capability_profile, + extract_sybase_version_value, + list_sybase_available_features, + merge_sybase_table_lists, + resolve_sybase_feature_flag, +) +from sqlspec.driver import SyncDataDictionaryBase +from sqlspec.utils.logging import get_logger, log_with_context + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from sqlspec.adapters.sybase.driver import SybaseDriver + +__all__ = ( + "SYBASE_CONFIG", + "SybaseDataDictionary", + "SybaseSyncDataDictionary", + "SybaseVersionInfo", + "build_column_metadata", + "build_foreign_key_metadata", + "build_index_metadata", + "parse_sybase_version", +) + +logger = get_logger("sqlspec.adapters.sybase.data_dictionary") + + +def parse_sybase_version(row: "dict[str, Any] | None") -> "SybaseVersionInfo | None": + """Parse the Sybase ASE version banner returned by the version query.""" + version_str = extract_sybase_version_value(row) + match = SYBASE_VERSION_PATTERN.search(version_str) if version_str else None + if match is None: + log_with_context(logger, logging.DEBUG, "data_dictionary.version.unparsed", dialect="sybase", value=version_str) + return None + sp_num = int(match.group(3)) if match.group(3) else 0 + sp_label = f"SP{sp_num:02d}" if match.group(3) else None + return SybaseVersionInfo(int(match.group(1)), int(match.group(2)), sp_num, service_pack=sp_label) + + +def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetadata]": + """Convert Sybase syscolumns query rows to column metadata.""" + return [ + ColumnMetadata( + schema_name=str(r.get("schema_name", "")), + table_name=str(r.get("table_name", "")), + column_name=str(r.get("column_name", "")), + data_type=str(r.get("data_type", "")), + is_nullable=bool(r.get("is_nullable", True)), + column_default=str(r["column_default"]) if r.get("column_default") is not None else None, + ordinal_position=int(r.get("ordinal_position", 1)), + max_length=int(r.get("max_length", 0)) if r.get("max_length") is not None else 0, + numeric_scale=int(r.get("numeric_scale", 0)) if r.get("numeric_scale") is not None else 0, + is_primary=bool(r.get("is_primary")), + is_unique=bool(r.get("is_unique")), + identity_generation=str(r["identity_generation"]) + if r.get("identity_generation") + else ("ALWAYS" if r.get("is_identity") else None), + is_generated=bool(r.get("is_generated")), + ) + for r in rows + ] + + +def build_index_metadata(rows: "Iterable[dict[str, Any]]") -> "list[IndexMetadata]": + """Group per-column Sybase sysindexes rows into index metadata.""" + indexes_map: dict[tuple[str, str, str], dict[str, Any]] = {} + for r in rows: + key = (str(r.get("schema_name", "")), str(r.get("table_name", "")), str(r.get("index_name", ""))) + if key not in indexes_map: + indexes_map[key] = { + "schema_name": key[0], + "table_name": key[1], + "index_name": key[2], + "columns": [], + "is_unique": bool(r.get("is_unique", 0)), + "is_primary": bool(r.get("is_primary", 0)), + } + col_name = str(r.get("column_name", "")) + if col_name: + indexes_map[key]["columns"].append(col_name) + + return [ + IndexMetadata( + schema_name=v["schema_name"], + table_name=v["table_name"], + index_name=v["index_name"], + columns=v["columns"], + is_unique=v["is_unique"], + is_primary=v["is_primary"], + ) + for v in indexes_map.values() + ] + + +def build_foreign_key_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ForeignKeyMetadata]": + """Convert Sybase sysreferences query rows to foreign key metadata.""" + return [ + ForeignKeyMetadata( + schema=str(r.get("schema_name", "")), + table_name=str(r.get("table_name", "")), + constraint_name=str(r.get("constraint_name", "")), + column_name=str(r.get("column_name", "")), + referenced_schema=str(r.get("referenced_schema", "")), + referenced_table=str(r.get("referenced_table", "")), + referenced_column=str(r.get("referenced_column", "")), + ) + for r in rows + ] + + +@mypyc_attr(allow_interpreted_subclasses=True, native_class=False) +class SybaseSyncDataDictionary(SyncDataDictionaryBase): + """Sybase ASE sync data dictionary for metadata reflection.""" + + dialect: ClassVar[str] = "sybase" + + def __init__(self) -> None: + """Initialize Sybase ASE sync data dictionary.""" + super().__init__() + + def get_metadata_capabilities( + self, driver: "SybaseDriver", domains: "Sequence[str] | None" = None + ) -> MetadataCapabilityProfile: + """Get Sybase ASE data-dictionary capability profile.""" + _ = driver + return build_sybase_metadata_capability_profile(type(self).__name__, domains) + + def get_version(self, driver: "SybaseDriver") -> SybaseVersionInfo | None: + """Get Sybase ASE database version information.""" + driver_id = id(driver) + if driver_id in self._version_fetch_attempted: + return cast("SybaseVersionInfo | None", self._version_cache.get(driver_id)) + + version_info = parse_sybase_version(driver.select_one_or_none(self.get_query("version", "current"))) + self.cache_version(driver_id, version_info) + return version_info + + def get_feature_flag(self, driver: "SybaseDriver", feature: str) -> bool: + """Check whether Sybase ASE supports a feature.""" + version_info = self.get_version(driver) + return resolve_sybase_feature_flag(feature, version_info) + + def get_optimal_type(self, driver: "SybaseDriver", type_category: str) -> str: + """Get optimal Sybase ASE type for a category.""" + _ = driver + return SYBASE_CONFIG.get_optimal_type(type_category) + + def list_available_features(self) -> list[str]: + """List available feature flags for this dialect.""" + return list_sybase_available_features() + + def get_tables(self, driver: "SybaseDriver", schema: str | None = None) -> list[TableMetadata]: + """Get tables sorted by dependency order with catalog fallback.""" + schema_name = self.resolve_schema(schema) + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="tables") + + ordered_rows = cast( + "list[TableMetadata]", + driver.select( + self.get_query("tables", "by_schema"), + schema_name=schema_name, + table_name=None, + schema_type=TableMetadata, + ), + ) + all_rows = cast( + "list[TableMetadata]", + driver.select( + self.get_query("tables", "all_by_schema"), + schema_name=schema_name, + table_name=None, + schema_type=TableMetadata, + ), + ) + return merge_sybase_table_lists(ordered_rows, all_rows) + + def get_columns( + self, driver: "SybaseDriver", table: str | None = None, schema: str | None = None + ) -> list[ColumnMetadata]: + """Get columns for a table or schema from dbo.syscolumns.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="columns") + rows = driver.select(self.get_query("columns", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="columns") + rows = driver.select(self.get_query("columns", "by_table"), schema_name=schema_name, table_name=table_name) + + return build_column_metadata(rows) + + def get_indexes( + self, driver: "SybaseDriver", table: str | None = None, schema: str | None = None + ) -> list[IndexMetadata]: + """Get indexes for a table or schema from dbo.sysindexes.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="indexes") + rows = driver.select(self.get_query("indexes", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="indexes") + rows = driver.select(self.get_query("indexes", "by_table"), schema_name=schema_name, table_name=table_name) + + return build_index_metadata(rows) + + def get_foreign_keys( + self, driver: "SybaseDriver", table: str | None = None, schema: str | None = None + ) -> list[ForeignKeyMetadata]: + """Get foreign keys from dbo.sysreferences.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="foreign_keys") + rows = driver.select(self.get_query("foreign_keys", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="foreign_keys") + rows = driver.select( + self.get_query("foreign_keys", "by_table"), schema_name=schema_name, table_name=table_name + ) + + return build_foreign_key_metadata(rows) + + def get_constraints( + self, driver: "SybaseDriver", table: str | None = None, schema: str | None = None + ) -> MetadataResult: + """Get Sybase ASE constraint metadata.""" + schema_name = self.resolve_schema(schema) + table_name = self.resolve_identifier(table) if table is not None else None + query = self.get_query("constraints", "by_table" if table_name else "by_schema") + rows = driver.select(query, schema_name=schema_name, table_name=table_name) + return MetadataResult("constraints", items=tuple(rows)) + + def get_views(self, driver: "SybaseDriver", schema: str | None = None) -> MetadataResult: + """Get Sybase ASE view metadata.""" + schema_name = self.resolve_schema(schema) + query = self.get_query("views", "by_schema") + rows = driver.select(query, schema_name=schema_name, view_name=None) + return MetadataResult("views", items=tuple(rows)) + + def get_objects(self, driver: "SybaseDriver", schema: "str | None" = None) -> MetadataResult: + """Get tables, views, and procedures from the Sybase ASE catalog.""" + rows = driver.select( + self.get_query("objects", "by_schema"), schema_name=self.resolve_schema(schema), object_name=None + ) + return MetadataResult("objects", items=tuple(rows)) + + def get_schemas(self, driver: "SybaseDriver") -> MetadataResult: + """Get Sybase ASE schema metadata.""" + query = self.get_query("schemas", "by_schema") + rows = driver.select(query, schema_name=None) + return MetadataResult("schemas", items=tuple(rows)) + + +SybaseDataDictionary = SybaseSyncDataDictionary diff --git a/sqlspec/adapters/sybase/driver.py b/sqlspec/adapters/sybase/driver.py new file mode 100644 index 000000000..1ee49ea2b --- /dev/null +++ b/sqlspec/adapters/sybase/driver.py @@ -0,0 +1,385 @@ +"""Sybase ASE database driver implementation over pymssql (FreeTDS TDS 5.0).""" + +import contextlib +from collections.abc import Sized +from typing import TYPE_CHECKING, Any, cast + +from sqlspec.adapters.sybase._typing import ( + SybaseConnection, + SybaseCursor, + SybaseError, + SybaseRawCursor, + SybaseSessionContext, +) +from sqlspec.adapters.sybase.core import ( + build_insert_statement, + collect_rows, + create_mapped_exception, + default_statement_config, + driver_profile, + escape_literal_percent, + format_identifier, + normalize_execute_many_parameters, + normalize_execute_parameters, + resolve_column_names, + resolve_many_rowcount, + resolve_rowcount, +) +from sqlspec.adapters.sybase.data_dictionary import SybaseSyncDataDictionary +from sqlspec.core import SQL, StatementConfig, get_cache_config, register_driver_profile +from sqlspec.driver import ( + BaseSyncExceptionHandler, + ExecutionResult, + SyncDriverAdapterBase, + SyncRowStream, + rows_to_dicts, + validate_savepoint_name, +) +from sqlspec.exceptions import SQLSpecError +from sqlspec.utils.logging import get_logger + +if TYPE_CHECKING: + from collections.abc import Sequence + + from sqlspec.adapters.sybase._typing import SybaseQueryParams as QueryParams + from sqlspec.builder import QueryBuilder + from sqlspec.core import ArrowResult, SQLResult, Statement, StatementFilter + from sqlspec.driver import CachedQuery + from sqlspec.storage import StorageBridgeJob, StorageDestination, StorageFormat, StorageTelemetry + from sqlspec.typing import StatementParameters + +__all__ = ("SybaseCursor", "SybaseDriver", "SybaseExceptionHandler", "SybaseSessionContext", "SybaseStreamSource") + +logger = get_logger("sqlspec.adapters.sybase") + + +class SybaseExceptionHandler(BaseSyncExceptionHandler): + """Context manager for handling Sybase pymssql exceptions.""" + + __slots__ = () + + def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: + if exc_type is None: + return False + if isinstance(exc_val, SybaseError): + self.pending_exception = create_mapped_exception(cast("Exception", exc_val), logger=logger) + return True + return False + + +class SybaseStreamSource: + """Native Sybase pymssql chunk source backed by ``cursor.fetchmany()``.""" + + __slots__ = ("_chunk_size", "_column_names", "_cursor_manager", "_driver", "_parameters", "_sql") + + def __init__(self, driver: "SybaseDriver", sql: str, parameters: Any, chunk_size: int) -> None: + self._driver = driver + self._sql = sql + self._parameters = parameters + self._chunk_size = chunk_size + self._cursor_manager: SybaseCursor | None = None + self._column_names: list[str] | None = None + + def start(self) -> None: + cursor_manager = self._driver.with_cursor(self._driver.connection) + try: + cursor = cursor_manager.__enter__() + handler = self._driver.handle_database_exceptions() + with handler: + normalized_params = normalize_execute_parameters(self._parameters) + sql = escape_literal_percent( + self._sql, normalized_params, self._driver.statement_config.parameter_validator + ) + cursor.execute(sql, normalized_params) + self._driver._check_pending_exception(handler) + except BaseException: + with contextlib.suppress(Exception): + cursor_manager.__exit__(None, None, None) + raise + self._cursor_manager = cursor_manager + + def fetch_chunk(self) -> "list[dict[str, Any]]": + cursor_manager = self._cursor_manager + if cursor_manager is None or cursor_manager.cursor is None: + return [] + cursor = cursor_manager.cursor + handler = self._driver.handle_database_exceptions() + rows: Any = [] + with handler: + rows = cursor.fetchmany(self._chunk_size) + self._driver._check_pending_exception(handler) + if not rows: + return [] + column_names = self._column_names + if column_names is None: + column_names = resolve_column_names(cursor.description or None, self._driver._column_name_cache) + self._column_names = column_names + return rows_to_dicts(rows, column_names) + + def close(self, error: bool = False) -> None: + _ = error + cursor_manager = self._cursor_manager + self._cursor_manager = None + if cursor_manager is not None: + with contextlib.suppress(Exception): + cursor_manager.__exit__(None, None, None) + + +class SybaseDriver(SyncDriverAdapterBase): + """Sybase Adaptive Server Enterprise database driver using pymssql over FreeTDS TDS 5.0.""" + + __slots__ = ("_column_name_cache", "_data_dictionary", "_explicit_transaction", "_transaction_active") + dialect = "sybase" + + def __init__( + self, + connection: "SybaseConnection", + statement_config: "StatementConfig | None" = None, + driver_features: "dict[str, Any] | None" = None, + ) -> None: + if statement_config is None: + statement_config = default_statement_config.replace( + enable_caching=get_cache_config().compiled_cache_enabled + ) + + super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features) + self._data_dictionary: SybaseSyncDataDictionary | None = None + self._column_name_cache: dict[int, tuple[Any, list[str]]] = {} + self._transaction_active = False + self._explicit_transaction = False + + @property + def data_dictionary(self) -> "SybaseSyncDataDictionary": + if self._data_dictionary is None: + self._data_dictionary = SybaseSyncDataDictionary() + return self._data_dictionary + + def dispatch_execute(self, cursor: "SybaseRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + normalized_params = normalize_execute_parameters(prepared_parameters) + sql = escape_literal_percent(sql, normalized_params, self.statement_config.parameter_validator) + cursor.execute(sql, normalized_params) + + if statement.returns_rows(): + fetched_data = cursor.fetchall() + description = cursor.description or None + rows, column_names, row_format = collect_rows(fetched_data, description, self._column_name_cache) + return self.create_execution_result( + cursor, + selected_data=rows, + column_names=column_names, + data_row_count=len(rows), + is_select_result=True, + row_format=row_format, + ) + + return self.create_execution_result(cursor, rowcount_override=resolve_rowcount(cursor)) + + def dispatch_execute_many(self, cursor: "SybaseRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + + prepared_parameters = normalize_execute_many_parameters(prepared_parameters) + sql = escape_literal_percent(sql, prepared_parameters, self.statement_config.parameter_validator) + parameter_count = len(prepared_parameters) if isinstance(prepared_parameters, Sized) else None + cursor.executemany(sql, cast("Sequence[QueryParams]", prepared_parameters)) + + affected_rows = resolve_many_rowcount(cursor, prepared_parameters, fallback_count=parameter_count) + return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True) + + def dispatch_execute_script(self, cursor: "SybaseRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + statements = self.split_script_statements(sql, statement.statement_config, strip_trailing_semicolon=True) + normalized_params = normalize_execute_parameters(prepared_parameters) + + successful_count = 0 + for stmt in statements: + stmt_sql = escape_literal_percent(stmt, normalized_params, self.statement_config.parameter_validator) + cursor.execute(stmt_sql, normalized_params) + successful_count += 1 + return self.create_execution_result( + cursor, statement_count=len(statements), successful_statements=successful_count, is_script_result=True + ) + + def begin(self) -> None: + """Begin a transaction on the Sybase ASE connection.""" + try: + explicit = bool(getattr(self.connection, "autocommit_state", True)) + if explicit: + with SybaseCursor(self.connection) as cursor: + cursor.execute("BEGIN TRANSACTION") + self._explicit_transaction = explicit + self._transaction_active = True + except SybaseError as exc: + msg = f"Failed to begin Sybase ASE transaction: {exc}" + raise SQLSpecError(msg) from exc + + def commit(self) -> None: + try: + if self._explicit_transaction: + with SybaseCursor(self.connection) as cursor: + cursor.execute("IF @@TRANCOUNT > 0 COMMIT TRANSACTION") + else: + self.connection.commit() + self._explicit_transaction = False + self._transaction_active = False + except SybaseError as exc: + msg = f"Failed to commit Sybase ASE transaction: {exc}" + raise SQLSpecError(msg) from exc + + def rollback(self) -> None: + try: + if self._explicit_transaction: + with SybaseCursor(self.connection) as cursor: + cursor.execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION") + else: + self.connection.rollback() + except SybaseError as exc: + msg = f"Failed to rollback Sybase ASE transaction: {exc}" + raise SQLSpecError(msg) from exc + finally: + self._explicit_transaction = False + self._transaction_active = False + + def with_cursor(self, connection: "SybaseConnection") -> "SybaseCursor": + return SybaseCursor(connection) + + def handle_database_exceptions(self) -> "SybaseExceptionHandler": + return SybaseExceptionHandler() + + def dispatch_select_stream(self, statement: "SQL", chunk_size: int) -> "SyncRowStream[dict[str, Any]] | None": + """Return a native Sybase pymssql row stream backed by ``fetchmany()``.""" + if not statement.returns_rows(): + return None + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + return SyncRowStream(SybaseStreamSource(self, sql, prepared_parameters, chunk_size)) + + def create_savepoint(self, name: str) -> None: + self.execute_script(f"SAVE TRANSACTION {validate_savepoint_name(name)}") + + def release_savepoint(self, name: str) -> None: + validate_savepoint_name(name) + + def rollback_to_savepoint(self, name: str) -> None: + self.execute_script(f"ROLLBACK TRANSACTION {validate_savepoint_name(name)}") + + def has_schema(self, schema: str) -> bool: + """Return whether the specified user/schema exists in dbo.sysusers.""" + result = False + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.execute("SELECT 1 FROM dbo.sysusers WHERE name = %s", (schema,)) + result = cursor.fetchone() is not None + self._check_pending_exception(exc_handler) + return result + + def select_to_storage( + self, + statement: "Statement | QueryBuilder | SQL | str", + destination: "StorageDestination", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + partitioner: "dict[str, object] | None" = None, + format_hint: "StorageFormat | None" = None, + telemetry: "StorageTelemetry | None" = None, + **kwargs: Any, + ) -> "StorageBridgeJob": + """Execute a query and write the Arrow result to storage.""" + self._require_capability("arrow_export_enabled") + arrow_result = self.select_to_arrow(statement, *parameters, statement_config=statement_config, **kwargs) + pipeline = self._storage_pipeline() + telemetry_payload = self._write_storage_result( + arrow_result, destination, format_hint=format_hint, pipeline=pipeline + ) + self._attach_partition_telemetry(telemetry_payload, partitioner) + return self._storage_job(telemetry_payload, telemetry) + + def load_from_arrow( + self, + table: str, + source: "ArrowResult | Any", + *, + partitioner: "dict[str, object] | None" = None, + overwrite: bool = False, + telemetry: "StorageTelemetry | None" = None, + ) -> "StorageBridgeJob": + """Load Arrow data into the target Sybase ASE table via executemany.""" + self._require_capability("arrow_import_enabled") + arrow_table = self._coerce_arrow_table(source) + if overwrite: + statement = f"DELETE FROM {format_identifier(table)}" + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.execute(statement) + self._check_pending_exception(exc_handler) + + columns, records = self._arrow_table_to_rows(arrow_table) + if records: + insert_sql = escape_literal_percent( + build_insert_statement(table, columns), records, self.statement_config.parameter_validator + ) + needs_preparation = self._arrow_rows_need_preparation(arrow_table) + prepared_records = ( + self.prepare_driver_parameters(records, self.statement_config, is_many=True) + if needs_preparation + else records + ) + normalized_records = normalize_execute_many_parameters(prepared_records) + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.executemany(insert_sql, cast("Sequence[QueryParams]", normalized_records)) + self._check_pending_exception(exc_handler) + + telemetry_payload = self._ingest_telemetry(arrow_table) + telemetry_payload["destination"] = table + self._attach_partition_telemetry(telemetry_payload, partitioner) + return self._storage_job(telemetry_payload, telemetry) + + def load_from_storage( + self, + table: str, + source: "StorageDestination", + *, + file_format: "StorageFormat", + partitioner: "dict[str, object] | None" = None, + overwrite: bool = False, + ) -> "StorageBridgeJob": + """Load staged artifacts from storage into the target Sybase ASE table.""" + arrow_table, inbound = self._read_storage_arrow(source, file_format=file_format) + return self.load_from_arrow(table, arrow_table, partitioner=partitioner, overwrite=overwrite, telemetry=inbound) + + def collect_rows(self, cursor: "SybaseRawCursor", fetched: "list[Any]") -> "tuple[list[Any], list[str], int]": + rows, column_names, _row_format = collect_rows(fetched, cursor.description or None, self._column_name_cache) + return rows, column_names, len(rows) + + def resolve_rowcount(self, cursor: "SybaseRawCursor") -> int: + return resolve_rowcount(cursor) + + def _execute_cache_hit( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" + ) -> "SQLResult": + normalized_params = normalize_execute_parameters(params) if isinstance(params, list) else params + if not normalized_params or ( + "%" in cached.compiled_sql + and escape_literal_percent( + cached.compiled_sql, normalized_params, self.statement_config.parameter_validator + ) + != cached.compiled_sql + ): + statement = self._cached_statement( + sql, + normalized_params, + cached, + normalized_params or None, + params_are_simple=True, + compiled_sql=cached.compiled_sql, + ) + return self._execute_cached_statement(statement) + return super()._execute_cache_hit(sql, normalized_params, cached) + + def _connection_in_transaction(self) -> bool: + """Return whether a transaction opened by this driver remains active.""" + return self._transaction_active + + +register_driver_profile("sybase", driver_profile) diff --git a/sqlspec/adapters/sybase/events/__init__.py b/sqlspec/adapters/sybase/events/__init__.py new file mode 100644 index 000000000..7bf12a0d3 --- /dev/null +++ b/sqlspec/adapters/sybase/events/__init__.py @@ -0,0 +1,5 @@ +"""Sybase ASE event extension.""" + +from sqlspec.adapters.sybase.events.store import SybaseEventQueueStore, SybaseSyncEventQueueStore + +__all__ = ("SybaseEventQueueStore", "SybaseSyncEventQueueStore") diff --git a/sqlspec/adapters/sybase/events/store.py b/sqlspec/adapters/sybase/events/store.py new file mode 100644 index 000000000..8f91608df --- /dev/null +++ b/sqlspec/adapters/sybase/events/store.py @@ -0,0 +1,55 @@ +"""Sybase ASE event queue store with ASE-compatible DDL.""" + +from sqlspec.adapters.sybase.config import SybaseConfig +from sqlspec.adapters.sybase.core import escape_sql_literal, quote_bracket_identifier, split_schema_and_table +from sqlspec.extensions.events import BaseEventQueueStore + +__all__ = ("SybaseEventQueueStore", "SybaseSyncEventQueueStore") + +_VARCHAR_TEXT_THRESHOLD = 4000 + + +class SybaseSyncEventQueueStore(BaseEventQueueStore[SybaseConfig]): + """Sybase ASE DDL hooks for the event queue store.""" + + __slots__ = () + + def _column_types(self) -> tuple[str, str, str]: + return "TEXT", "TEXT", "BIGDATETIME" + + def _string_type(self, length: int) -> str: + if length >= _VARCHAR_TEXT_THRESHOLD: + return "TEXT" + return f"VARCHAR({length})" + + def _integer_type(self) -> str: + return "INT" + + def _timestamp_default(self) -> str: + return "GETUTCDATE()" + + def _wrap_create_statement(self, statement: str, object_type: str) -> str: + object_name = escape_sql_literal(_object_name(self.table_name)) + escaped_statement = escape_sql_literal(statement.rstrip().rstrip(";")) + if object_type == "table": + return f"IF OBJECT_ID('{object_name}', 'U') IS NULL EXECUTE('{escaped_statement}')" + if object_type == "index": + index_name = escape_sql_literal(self._index_name()) + return ( + f"IF NOT EXISTS (SELECT 1 FROM dbo.sysindexes WHERE name = '{index_name}' " + f"AND id = OBJECT_ID('{object_name}')) EXECUTE('{escaped_statement}')" + ) + return statement + + def _wrap_drop_statement(self, statement: str) -> str: + object_name = escape_sql_literal(_object_name(self.table_name)) + escaped_statement = escape_sql_literal(statement.rstrip().rstrip(";")) + return f"IF OBJECT_ID('{object_name}', 'U') IS NOT NULL EXECUTE('{escaped_statement}');" + + +SybaseEventQueueStore = SybaseSyncEventQueueStore + + +def _object_name(table_name: str) -> str: + schema_name, bare_table_name = split_schema_and_table(table_name) + return f"{quote_bracket_identifier(schema_name)}.{quote_bracket_identifier(bare_table_name)}" diff --git a/sqlspec/adapters/sybase/litestar/__init__.py b/sqlspec/adapters/sybase/litestar/__init__.py new file mode 100644 index 000000000..800850dd5 --- /dev/null +++ b/sqlspec/adapters/sybase/litestar/__init__.py @@ -0,0 +1,5 @@ +"""Sybase ASE Litestar extension.""" + +from sqlspec.adapters.sybase.litestar.store import SybaseStore + +__all__ = ("SybaseStore",) diff --git a/sqlspec/adapters/sybase/litestar/store.py b/sqlspec/adapters/sybase/litestar/store.py new file mode 100644 index 000000000..e81a4fe0d --- /dev/null +++ b/sqlspec/adapters/sybase/litestar/store.py @@ -0,0 +1,241 @@ +"""Sybase ASE Litestar Store implementation.""" + +from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING, Any + +from sqlspec.adapters.sybase._typing import SybaseCursor +from sqlspec.adapters.sybase.core import escape_sql_literal, quote_bracket_identifier, split_schema_and_table +from sqlspec.extensions.litestar.store import BaseSQLSpecStore +from sqlspec.utils.sync_tools import async_ + +if TYPE_CHECKING: + from sqlspec.adapters.sybase.config import SybaseConfig + +__all__ = ("SybaseStore",) + + +class SybaseStore(BaseSQLSpecStore["SybaseConfig"]): + """Sybase ASE-backed session store using pymssql sync sessions.""" + + __slots__ = () + + def __init__(self, config: "SybaseConfig") -> None: + super().__init__(config) + + async def create_table(self) -> None: + """Create the session table if it doesn't exist.""" + if not self.create_schema_enabled: + await self.reconcile_schema() + return + await async_(self._create_table)() + await self.reconcile_schema(assume_existing=True) + + async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + """Get a session value by key.""" + return await async_(self._get)(key, renew_for) + + async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None: + """Store a session value.""" + await async_(self._set)(key, value, expires_in) + + async def delete(self, key: str) -> None: + """Delete a session by key.""" + await async_(self._delete)(key) + + async def delete_all(self) -> None: + """Delete all sessions from the store.""" + await async_(self._delete_all)() + + async def exists(self, key: str) -> bool: + """Check if a session key exists and is not expired.""" + return await async_(self._exists)(key) + + async def expires_in(self, key: str) -> "int | None": + """Get the time in seconds until the session expires.""" + return await async_(self._expires_in)(key) + + async def delete_expired(self) -> int: + """Delete all expired sessions.""" + return await async_(self._delete_expired)() + + def _table_ddl(self) -> str: + """Get Sybase ASE CREATE TABLE SQL with OBJECT_ID existence guards.""" + _, table_name = split_schema_and_table(self._table_name) + table_ref = _table_ref(self._table_name) + escaped_table_ref = escape_sql_literal(table_ref) + index_bare = f"IX_{table_name}_expires_at" + escaped_index_bare = escape_sql_literal(index_bare) + index_name = quote_bracket_identifier(index_bare) + create_table_sql = escape_sql_literal( + f"CREATE TABLE {table_ref} (" + "session_id VARCHAR(255) NOT NULL PRIMARY KEY, " + "data IMAGE NOT NULL, " + "expires_at BIGDATETIME NULL, " + "created_at BIGDATETIME DEFAULT GETUTCDATE() NOT NULL, " + "updated_at BIGDATETIME DEFAULT GETUTCDATE() NOT NULL" + ")" + ) + create_index_sql = escape_sql_literal(f"CREATE INDEX {index_name} ON {table_ref}(expires_at)") + return ( + f"IF OBJECT_ID('{escaped_table_ref}', 'U') IS NULL EXECUTE('{create_table_sql}');\n" + f"IF NOT EXISTS (SELECT 1 FROM dbo.sysindexes WHERE name = '{escaped_index_bare}' " + f"AND id = OBJECT_ID('{escaped_table_ref}')) EXECUTE('{create_index_sql}');" + ) + + def _drop_table_sql(self) -> "list[str]": + """Get Sybase ASE DROP TABLE statements.""" + table_ref = _table_ref(self._table_name) + escaped = escape_sql_literal(table_ref) + return [f"IF OBJECT_ID('{escaped}', 'U') IS NOT NULL EXECUTE('DROP TABLE {escaped}');"] + + def _create_table(self) -> None: + with self._config.provide_session() as driver: + driver.execute_script(self._table_ddl()) + driver.commit() + self._log_table_created() + + def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + table_ref = _table_ref(self._table_name) + sql = f""" + SELECT data, expires_at FROM {table_ref} + WHERE session_id = %s + AND (expires_at IS NULL OR expires_at > GETUTCDATE()) + """ + with self._config.provide_connection() as conn: + with SybaseCursor(conn) as cursor: + cursor.execute(sql, (key,)) + row = cursor.fetchone() + + if row is None: + return None + + expires_at = _normalize_utc(_row_value(row, "expires_at", 1)) + if renew_for is not None and expires_at is not None: + new_expires_at = self._calculate_expires_at(renew_for) + if new_expires_at is not None: + with SybaseCursor(conn) as update_cursor: + update_cursor.execute( + f""" + UPDATE {table_ref} + SET expires_at = %s, updated_at = GETUTCDATE() + WHERE session_id = %s + """, + (new_expires_at, key), + ) + conn.commit() + + return _coerce_bytes(_row_value(row, "data", 0)) + + def _set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None: + data = self._value_to_bytes(value) + expires_at = self._calculate_expires_at(expires_in) + table_ref = _table_ref(self._table_name) + sql = f""" + IF EXISTS (SELECT 1 FROM {table_ref} HOLDLOCK WHERE session_id = %s) + UPDATE {table_ref} + SET data = %s, expires_at = %s, updated_at = GETUTCDATE() + WHERE session_id = %s + ELSE + INSERT INTO {table_ref} (session_id, data, expires_at) + VALUES (%s, %s, %s) + """ + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(sql, (key, data, expires_at, key, key, data, expires_at)) + conn.commit() + + def _delete(self, key: str) -> None: + table_ref = _table_ref(self._table_name) + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(f"DELETE FROM {table_ref} WHERE session_id = %s", (key,)) + conn.commit() + + def _delete_all(self) -> None: + table_ref = _table_ref(self._table_name) + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(f"TRUNCATE TABLE {table_ref}") + conn.commit() + self._log_delete_all() + + def _exists(self, key: str) -> bool: + table_ref = _table_ref(self._table_name) + sql = f""" + SELECT 1 + FROM {table_ref} + WHERE session_id = %s + AND (expires_at IS NULL OR expires_at > GETUTCDATE()) + """ + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(sql, (key,)) + return cursor.fetchone() is not None + + def _expires_in(self, key: str) -> "int | None": + table_ref = _table_ref(self._table_name) + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(f"SELECT expires_at FROM {table_ref} WHERE session_id = %s", (key,)) + row = cursor.fetchone() + + if row is None: + return None + expires_at = _normalize_utc(_row_value(row, "expires_at", 0)) + if expires_at is None: + return None + remaining = expires_at - datetime.now(UTC) + return max(0, int(remaining.total_seconds())) + + def _delete_expired(self) -> int: + table_ref = _table_ref(self._table_name) + sql = f""" + DELETE FROM {table_ref} + WHERE expires_at IS NOT NULL + AND expires_at < GETUTCDATE() + """ + with self._config.provide_connection() as conn, SybaseCursor(conn) as cursor: + cursor.execute(sql) + count = int(getattr(cursor, "rowcount", 0) or 0) + conn.commit() + if count > 0: + self._log_delete_expired(count) + return count + + +def _table_ref(table_name: str) -> str: + schema_name, bare_table_name = split_schema_and_table(table_name) + return f"{quote_bracket_identifier(schema_name)}.{quote_bracket_identifier(bare_table_name)}" + + +def _row_value(row: object, key: str, index: int) -> Any: + """Return a value from dict-like or sequence-like driver rows.""" + if isinstance(row, dict): + if key in row: + return row[key] + upper_key = key.upper() + if upper_key in row: + return row[upper_key] + return None + if isinstance(row, (list, tuple)) and len(row) > index: + return row[index] + return getattr(row, key, None) + + +def _normalize_utc(value: Any) -> "datetime | None": + if value is None: + return None + if not isinstance(value, datetime): + return None + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) + + +def _coerce_bytes(value: Any) -> bytes: + if value is None: + return b"" + if isinstance(value, bytes): + return value + if isinstance(value, bytearray): + return bytes(value) + if isinstance(value, memoryview): + return value.tobytes() + if isinstance(value, str): + return value.encode("utf-8") + return bytes(value) diff --git a/sqlspec/adapters/sybase/migrations.py b/sqlspec/adapters/sybase/migrations.py new file mode 100644 index 000000000..e6b8d3f2b --- /dev/null +++ b/sqlspec/adapters/sybase/migrations.py @@ -0,0 +1,166 @@ +"""Sybase ASE-specific migration tracker.""" + +import logging +import os +from contextlib import suppress +from typing import TYPE_CHECKING + +from sqlspec.adapters.sybase.core import ( + escape_sql_literal, + format_identifier, + quote_bracket_identifier, + split_schema_and_table, +) +from sqlspec.builder import CreateTable, sql +from sqlspec.migrations.tracker import SyncMigrationTracker +from sqlspec.migrations.version import parse_version +from sqlspec.observability import resolve_db_system +from sqlspec.utils.logging import get_logger, log_with_context + +if TYPE_CHECKING: + from sqlspec.driver import SyncDriverAdapterBase + +__all__ = ("SybaseMigrationTrackerMixin", "SybaseSyncMigrationTracker") + +logger = get_logger("sqlspec.migrations.sybase") + + +class SybaseMigrationTrackerMixin: + """Sybase ASE-specific migration table DDL and schema maintenance.""" + + __slots__ = () + + version_table: str + + def _tracking_table_ddl(self) -> CreateTable: + """Return Sybase ASE-compatible migration tracking table DDL.""" + return ( + sql + .create_table(self.version_table) + .column("version_num", "VARCHAR(32)", primary_key=True) + .column("version_type", "VARCHAR(16)") + .column("execution_sequence", "INT") + .column("description", "TEXT") + .column("applied_at", "BIGDATETIME", default="GETUTCDATE()", not_null=True) + .column("execution_time_ms", "INT") + .column("checksum", "VARCHAR(64)") + .column("applied_by", "VARCHAR(255)") + .column("replaces", "TEXT") + ) + + def _idempotent_tracking_table_ddl_text(self) -> str: + """Wrap CREATE TABLE in a Sybase ASE OBJECT_ID existence probe with dynamic EXECUTE.""" + table_ref = escape_sql_literal(format_identifier(self.version_table)) + create_sql = escape_sql_literal(self._tracking_table_ddl_text().rstrip().rstrip(";")) + return f"IF OBJECT_ID('{table_ref}', 'U') IS NULL EXECUTE('{create_sql}');" + + def _tracking_table_ddl_text(self) -> str: + """Render CREATE TABLE text with Sybase ASE native column types.""" + column_lines: list[str] = [] + for column_def in self._tracking_table_ddl().columns: + default_clause = f" DEFAULT {column_def.default}" if column_def.default else "" + null_clause = " NOT NULL" if column_def.primary_key or column_def.not_null else " NULL" + primary_key_clause = " PRIMARY KEY" if column_def.primary_key else "" + column_lines.append( + f" {quote_bracket_identifier(column_def.name)} {column_def.dtype}{default_clause}{null_clause}{primary_key_clause}" + ) + return f"CREATE TABLE {format_identifier(self.version_table)} (\n" + ",\n".join(column_lines) + "\n)" + + def _existing_columns_query(self) -> str: + """Return Sybase ASE catalog query text for migration tracking table columns.""" + schema_name, table_name = split_schema_and_table(self.version_table) + return f""" + SELECT c.name AS column_name + FROM dbo.syscolumns c + INNER JOIN dbo.sysobjects o ON o.id = c.id AND o.type = 'U' + INNER JOIN dbo.sysusers u ON u.uid = o.uid + WHERE o.name = '{escape_sql_literal(table_name)}' + AND u.name = '{escape_sql_literal(schema_name)}' + """ + + def _add_column_statement_text(self, column_name: str) -> str | None: + """Return Sybase ASE ALTER TABLE text for a missing migration column.""" + target_create = self._tracking_table_ddl() + column_def = next((col for col in target_create.columns if col.name.lower() == column_name), None) + if column_def is None: + return None + default_clause = f" DEFAULT {column_def.default}" if column_def.default else "" + nullable_clause = " NOT NULL" if (column_def.not_null and column_def.default is not None) else " NULL" + return f"ALTER TABLE {format_identifier(self.version_table)} ADD {quote_bracket_identifier(column_def.name)} {column_def.dtype}{default_clause}{nullable_clause};" + + +class SybaseSyncMigrationTracker(SybaseMigrationTrackerMixin, SyncMigrationTracker): + """Sybase ASE sync migration tracker.""" + + def ensure_tracking_table(self, driver: "SyncDriverAdapterBase") -> None: + """Create the migration tracking table if it does not exist.""" + driver.execute_script(self._idempotent_tracking_table_ddl_text()) + self._commit_if_needed(driver) + self._migrate_schema_if_needed(driver) + + def record_migration( + self, + driver: "SyncDriverAdapterBase", + version: str, + description: str, + execution_time_ms: int, + checksum: str, + *, + commit: bool = True, + ) -> None: + """Record a successfully applied migration with Sybase ASE-compatible metadata.""" + parsed_version = parse_version(version) + version_type = parsed_version.type.value + result = driver.execute(self._next_execution_sequence_query()) + next_sequence = result.get_data()[0]["next_seq"] if result.data else 1 + driver.execute( + self._record_migration_statement( + version, + version_type, + next_sequence, + description, + execution_time_ms, + checksum, + os.environ.get("USER", "unknown"), + ) + ) + if commit: + self._commit_if_needed(driver) + + def _commit_if_needed(self, driver: "SyncDriverAdapterBase") -> None: + if hasattr(driver, "driver_features"): + self._safe_commit(driver) + return + driver.commit() + + def _migrate_schema_if_needed(self, driver: "SyncDriverAdapterBase") -> None: + """Check and add missing tracking table columns through Sybase ASE catalog tables.""" + try: + rows = driver.select(self._existing_columns_query()) + existing_columns = {str(row["column_name"]).lower() for row in rows if row.get("column_name") is not None} + missing_columns = self._detect_missing_columns(existing_columns) + if not missing_columns: + return + for column_name in sorted(missing_columns): + self._add_column(driver, column_name) + self._commit_if_needed(driver) + except Exception as exc: + with suppress(Exception): + driver.rollback() + log_with_context( + logger, + logging.ERROR, + "migration.track", + db_system=resolve_db_system(type(driver).__name__), + table=self.version_table, + operation="schema_check", + status="failed", + error_type=type(exc).__name__, + ) + + def _add_column(self, driver: "SyncDriverAdapterBase", column_name: str) -> None: + """Add a single missing migration tracking column.""" + add_column_sql = self._add_column_statement_text(column_name) + if add_column_sql is None: + return + driver.execute_script(add_column_sql) diff --git a/sqlspec/adapters/sybase/pool.py b/sqlspec/adapters/sybase/pool.py new file mode 100644 index 000000000..9dfc43a4f --- /dev/null +++ b/sqlspec/adapters/sybase/pool.py @@ -0,0 +1,265 @@ +"""Sybase ASE connection pool with thread-locked TDSVER='5.0' negotiation.""" + +import contextlib +import logging +import os +import threading +import time +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, cast + +from sqlspec.adapters.sybase._typing import PYMSSQL_MODULE, SybaseConnection +from sqlspec.adapters.sybase.core import DEFAULT_SYBASE_CONN_PROPERTIES, DEFAULT_SYBASE_TDS_VERSION +from sqlspec.utils.logging import POOL_LOGGER_NAME, get_logger, log_with_context +from sqlspec.utils.uuids import uuid4 + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + +__all__ = ("SybaseConnectionPool",) + +logger = get_logger(POOL_LOGGER_NAME) +_ADAPTER_NAME = "sybase" +_TDSVER_LOCK = threading.RLock() +pymssql = PYMSSQL_MODULE + + +class SybaseConnectionPool: + """Thread-local connection manager for Sybase ASE over pymssql (FreeTDS TDS 5.0).""" + + __slots__ = ( + "_checked_out", + "_connection_parameters", + "_connection_registry", + "_connection_threads", + "_generation", + "_health_check_interval", + "_on_connection_create", + "_pool_id", + "_recycle_seconds", + "_registry_lock", + "_tds_version", + "_thread_local", + ) + + def __init__( + self, + connection_parameters: "dict[str, Any]", + recycle_seconds: int = 86400, + health_check_interval: float = 30.0, + on_connection_create: "Callable[[SybaseConnection], None] | None" = None, + ) -> None: + """Initialize the thread-local Sybase ASE connection manager. + + Args: + connection_parameters: Normalized Sybase connection parameters. + recycle_seconds: Connection recycle time in seconds (default 24h). + health_check_interval: Seconds of idle time before running health check. + on_connection_create: Callback executed when a connection is created. + """ + params = dict(connection_parameters) + self._tds_version = str( + params.pop("_sybase_tds_version", params.pop("tds_version", DEFAULT_SYBASE_TDS_VERSION)) + ) + params.setdefault("conn_properties", DEFAULT_SYBASE_CONN_PROPERTIES) + params.setdefault("autocommit", True) + self._connection_parameters = params + self._thread_local = threading.local() + self._connection_registry: set[SybaseConnection] = set() + self._connection_threads: dict[SybaseConnection, threading.Thread] = {} + self._checked_out = 0 + self._generation = 0 + self._registry_lock = threading.Lock() + self._recycle_seconds = recycle_seconds + self._health_check_interval = health_check_interval + self._on_connection_create = on_connection_create + self._pool_id = str(uuid4())[:8] + + @property + def _database_name(self) -> str: + """Get sanitized database name for logging.""" + return str(self._connection_parameters.get("database", "unknown")) + + def new_connection(self) -> SybaseConnection: + """Open a standalone Sybase ASE connection using FreeTDS TDS 5.0 negotiation. + + Temporarily sets ``os.environ['TDSVER']`` inside a module-level re-entrant + lock so FreeTDS ``libsybdb`` sends a native TDS 5.0 login packet without + passing ``tds_version='5.0'`` through ``pymssql._mssql``'s Cython validator. + + Returns: + SybaseConnection: A newly opened, fully configured connection. + """ + with _TDSVER_LOCK: + prior_tdsver = os.environ.get("TDSVER") + os.environ["TDSVER"] = self._tds_version + try: + connection = pymssql.connect(**self._connection_parameters) + finally: + if prior_tdsver is None: + os.environ.pop("TDSVER", None) + else: + os.environ["TDSVER"] = prior_tdsver + + if self._on_connection_create is not None: + self._on_connection_create(connection) + + return cast("SybaseConnection", connection) + + @contextmanager + def get_connection(self) -> "Generator[SybaseConnection, None, None]": + """Get a thread-local connection. + + Yields: + A thread-local database connection. + """ + connection = self.acquire() + try: + yield connection + except Exception: + with contextlib.suppress(Exception): + self._close_thread_connection() + raise + finally: + self.release(connection) + + def close(self) -> None: + """Close every connection this pool opened, on any thread.""" + self._close_thread_connection() + with self._registry_lock: + orphaned = list(self._connection_registry) + self._connection_registry.clear() + self._connection_threads.clear() + self._checked_out = 0 + self._generation += 1 + for connection in orphaned: + with contextlib.suppress(Exception): + connection.close() + + def acquire(self) -> SybaseConnection: + connection = self._get_thread_connection() + with self._registry_lock: + self._checked_out += 1 + return connection + + def release(self, connection: SybaseConnection) -> None: + _ = connection + with self._registry_lock: + if self._checked_out > 0: + self._checked_out -= 1 + + def size(self) -> int: + self._prune_dead_thread_connections() + with self._registry_lock: + return len(self._connection_registry) + + def checked_out(self) -> int: + with self._registry_lock: + return self._checked_out + + def _prune_dead_thread_connections(self) -> None: + dead_connections: list[SybaseConnection] = [] + with self._registry_lock: + for conn, owner_thread in list(self._connection_threads.items()): + if not owner_thread.is_alive(): + self._connection_registry.discard(conn) + self._connection_threads.pop(conn, None) + dead_connections.append(conn) + for conn in dead_connections: + with contextlib.suppress(Exception): + conn.close() + + def _create_connection(self) -> SybaseConnection: + self._prune_dead_thread_connections() + connection = self.new_connection() + + with self._registry_lock: + self._connection_registry.add(connection) + self._connection_threads[connection] = threading.current_thread() + + return connection + + def _is_connection_alive(self, connection: SybaseConnection) -> bool: + try: + cursor = connection.cursor() + try: + cursor.execute("SELECT 1") + cursor.fetchone() + finally: + cursor.close() + except Exception: + return False + return True + + def _get_thread_connection(self) -> SybaseConnection: + thread_state = self._thread_local.__dict__ + if thread_state.get("generation") != self._generation: + stale = thread_state.pop("connection", None) + if stale is not None: + self._retire_connection(cast("SybaseConnection", stale)) + thread_state.pop("created_at", None) + thread_state.pop("last_used", None) + self._thread_local.generation = self._generation + if "connection" not in thread_state: + self._thread_local.connection = self._create_connection() + self._thread_local.created_at = time.time() + self._thread_local.last_used = time.time() + return cast("SybaseConnection", self._thread_local.connection) + + if self._recycle_seconds > 0 and time.time() - self._thread_local.created_at > self._recycle_seconds: + log_with_context( + logger, + logging.DEBUG, + "pool.connection.recycle", + adapter=_ADAPTER_NAME, + pool_id=self._pool_id, + database=self._database_name, + recycle_seconds=self._recycle_seconds, + reason="exceeded_recycle_time", + ) + self._retire_connection(cast("SybaseConnection", self._thread_local.connection)) + self._thread_local.connection = self._create_connection() + self._thread_local.created_at = time.time() + self._thread_local.last_used = time.time() + return cast("SybaseConnection", self._thread_local.connection) + + idle_time = time.time() - thread_state.get("last_used", 0) + if ( + self._health_check_interval > 0 + and idle_time > self._health_check_interval + and not self._is_connection_alive(self._thread_local.connection) + ): + log_with_context( + logger, + logging.DEBUG, + "pool.connection.recycle", + adapter=_ADAPTER_NAME, + pool_id=self._pool_id, + database=self._database_name, + idle_seconds=round(idle_time, 1), + reason="failed_health_check", + ) + self._retire_connection(cast("SybaseConnection", self._thread_local.connection)) + self._thread_local.connection = self._create_connection() + self._thread_local.created_at = time.time() + + self._thread_local.last_used = time.time() + return cast("SybaseConnection", self._thread_local.connection) + + def _retire_connection(self, connection: SybaseConnection) -> None: + """Close a pool-owned connection and drop it from the shutdown registry.""" + with self._registry_lock: + self._connection_registry.discard(connection) + self._connection_threads.pop(connection, None) + with contextlib.suppress(Exception): + connection.close() + + def _close_thread_connection(self) -> None: + thread_state = self._thread_local.__dict__ + if "connection" in thread_state: + self._retire_connection(cast("SybaseConnection", self._thread_local.connection)) + del self._thread_local.connection + if "created_at" in thread_state: + del self._thread_local.created_at + if "last_used" in thread_state: + del self._thread_local.last_used diff --git a/tests/unit/adapters/test_sybase.py b/tests/unit/adapters/test_sybase.py new file mode 100644 index 000000000..f8998d2b1 --- /dev/null +++ b/tests/unit/adapters/test_sybase.py @@ -0,0 +1,517 @@ +"""Unit tests for the Sybase ASE (pymssql over FreeTDS TDS 5.0) adapter.""" + +import os +import socket +import threading +from datetime import UTC, datetime +from typing import Any, cast +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from sqlspec.adapters.sybase import ( + DEFAULT_SYBASE_CONN_PROPERTIES, + DEFAULT_SYBASE_TDS_VERSION, + SybaseConfig, + SybaseConnection, + SybaseConnectionPool, + SybaseCursor, + SybaseDataDictionary, + SybaseDriver, + SybaseExceptionHandler, + SybaseSyncDataDictionary, + build_connection_config, + create_mapped_exception, + default_statement_config, + driver_profile, +) +from sqlspec.adapters.sybase._typing import SybaseRawCursor +from sqlspec.adapters.sybase.adk import SybaseADKConfig, SybaseADKMemoryStore, SybaseADKStore +from sqlspec.adapters.sybase.events import SybaseEventQueueStore, SybaseSyncEventQueueStore +from sqlspec.adapters.sybase.litestar import SybaseStore +from sqlspec.adapters.sybase.migrations import SybaseSyncMigrationTracker +from sqlspec.core import SQL, ParameterStyle +from sqlspec.data_dictionary.dialects.sybase import SybaseVersionInfo +from sqlspec.exceptions import ( + CheckViolationError, + DatabaseConnectionError, + DataError, + DeadlockError, + ForeignKeyViolationError, + NotFoundError, + NotNullViolationError, + OperationalError, + PermissionDeniedError, + QueryTimeoutError, + SQLParsingError, + TransactionError, + UniqueViolationError, +) + + +class FakeSybaseError(Exception): + """Fake pymssql error for Sybase unit tests.""" + + +class FakeCursor: + """Minimal DB-API cursor for Sybase unit tests.""" + + def __init__( + self, rows: "list[Any] | None" = None, description: "list[tuple[str, ...]] | None" = None, rowcount: int = -1 + ) -> None: + self.rows = rows or [] + self.description = description + self.rowcount = rowcount + self.closed = False + self.calls: list[tuple[str, Any]] = [] + self.many_calls: list[tuple[str, Any]] = [] + self.fetchmany_sizes: list[int] = [] + + def execute(self, sql: str, parameters: Any = None) -> None: + self.calls.append((sql, parameters)) + + def executemany(self, sql: str, parameters: Any = None) -> None: + self.many_calls.append((sql, parameters)) + + def fetchall(self) -> "list[Any]": + return self.rows + + def fetchmany(self, size: int) -> "list[Any]": + self.fetchmany_sizes.append(size) + chunk = self.rows[:size] + self.rows = self.rows[size:] + return chunk + + def fetchone(self) -> Any: + return self.rows[0] if self.rows else None + + def close(self) -> None: + self.closed = True + + +class FakeConnection: + """Minimal pymssql connection for Sybase unit tests.""" + + def __init__(self, cursor: "FakeCursor | None" = None) -> None: + self.cursor_obj = cursor or FakeCursor() + self.closed = False + self.commits = 0 + self.rollbacks = 0 + self.autocommit_values: list[bool] = [] + self.autocommit_state = True + + def cursor(self, *args: Any, **kwargs: Any) -> FakeCursor: + self.cursor_args = args + self.cursor_kwargs = kwargs + return self.cursor_obj + + def commit(self) -> None: + if not self.autocommit_state: + self.commits += 1 + + def rollback(self) -> None: + if not self.autocommit_state: + self.rollbacks += 1 + + def close(self) -> None: + self.closed = True + + def autocommit(self, value: bool) -> None: + self.autocommit_values.append(value) + self.autocommit_state = value + + +def test_build_connection_config_defaults_and_normalization() -> None: + """build_connection_config sets Sybase ASE defaults and strips tds_version='5.0' for pool TDSVER negotiation.""" + cfg = build_connection_config({ + "host": "ase.internal", + "username": "sa", + "db": "pubs2", + "tds_version": "5.0", + "extra": {"appname": "sqlspec_sybase", "ignored": None}, + }) + + assert cfg["server"] == "ase.internal" + assert "host" not in cfg + assert cfg["port"] == 5000 + assert cfg["user"] == "sa" + assert "username" not in cfg + assert cfg["database"] == "pubs2" + assert "db" not in cfg + assert cfg["charset"] == "UTF-8" + assert cfg["autocommit"] is True + assert cfg["conn_properties"] == DEFAULT_SYBASE_CONN_PROPERTIES + assert cfg["_sybase_tds_version"] == DEFAULT_SYBASE_TDS_VERSION + assert "tds_version" not in cfg + assert cfg["appname"] == "sqlspec_sybase" + assert "extra" not in cfg + + +def test_sybase_pool_sets_and_restores_tdsver_env(monkeypatch: pytest.MonkeyPatch) -> None: + """SybaseConnectionPool sets TDSVER='5.0' during connect and restores the prior environment value.""" + import sqlspec.adapters.sybase.pool as pool_module + + captured_tdsver: list[str | None] = [] + captured_kwargs: list[dict[str, Any]] = [] + fake_conn = FakeConnection() + + class FakeModule: + def connect(self, **kwargs: Any) -> FakeConnection: + captured_tdsver.append(os.environ.get("TDSVER")) + captured_kwargs.append(kwargs) + return fake_conn + + monkeypatch.setattr(pool_module, "pymssql", FakeModule()) + monkeypatch.setenv("TDSVER", "7.3") + + cfg = build_connection_config({"server": "ase.example.test", "user": "sa", "database": "master"}) + pool = SybaseConnectionPool(cfg) + conn = pool.acquire() + pool.release(conn) + + assert cast("object", conn) is fake_conn + assert captured_tdsver == ["5.0"] + assert os.environ.get("TDSVER") == "7.3" + assert "tds_version" not in captured_kwargs[0] + assert "_sybase_tds_version" not in captured_kwargs[0] + assert captured_kwargs[0]["conn_properties"] == DEFAULT_SYBASE_CONN_PROPERTIES + assert captured_kwargs[0]["autocommit"] is True + + +def test_sybase_pool_transmits_native_tds_50_login_packet_over_loopback() -> None: + """SybaseConnectionPool.new_connection transmits a native 619-byte TDS 5.0 login packet via FreeTDS.""" + received_packets: list[bytes] = [] + ready = threading.Event() + + def _listener(server_sock: socket.socket) -> None: + ready.set() + try: + conn, _ = server_sock.accept() + with conn: + conn.settimeout(2.0) + data = conn.recv(4096) + received_packets.append(data) + except OSError: + return + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_sock: + server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_sock.bind(("127.0.0.1", 0)) + port = server_sock.getsockname()[1] + server_sock.listen(1) + + listener_thread = threading.Thread(target=_listener, args=(server_sock,), daemon=True) + listener_thread.start() + ready.wait(timeout=2.0) + + cfg = build_connection_config({ + "server": "127.0.0.1", + "port": port, + "user": "sa", + "password": "secret", + "database": "master", + "login_timeout": 1, + "timeout": 1, + }) + pool = SybaseConnectionPool(cfg) + with pytest.raises(Exception): + pool.new_connection() + + listener_thread.join(timeout=2.0) + + assert len(received_packets) == 1 + packet = received_packets[0] + assert len(packet) >= 500 + assert packet[0] == 0x02 + assert 0xE2 in packet + + +@pytest.mark.parametrize( + ("code", "message", "expected_type"), + [ + (2601, "Attempt to insert duplicate key row in object 'users' with unique index (2601)", UniqueViolationError), + (2627, "Violation of UNIQUE KEY constraint (2627)", UniqueViolationError), + (546, "Foreign key constraint violation occurred, dbname = 'pubs2' (546)", ForeignKeyViolationError), + (547, "Dependent foreign key constraint violation (547)", ForeignKeyViolationError), + (547, "The UPDATE statement conflicted with the CHECK constraint (547)", CheckViolationError), + (548, "Check constraint violation occurred, dbname = 'pubs2' (548)", CheckViolationError), + (233, "The column id in table users does not allow null values (233)", NotNullViolationError), + (515, "Attempt to insert NULL value into column 'name' (515)", NotNullViolationError), + (1205, "Your server command (family id #0, process id #14) encountered a deadlock (1205)", DeadlockError), + (12205, "Could not acquire a lock within the specified wait period (12205)", QueryTimeoutError), + (102, "Incorrect syntax near 'FROM' (102)", SQLParsingError), + (156, "Incorrect syntax near the keyword 'SELECT' (156)", SQLParsingError), + (11021, "Function FOO invoked with wrong number or type of argument(s) (11021)", SQLParsingError), + (207, "Invalid column name 'missing_col' (207)", NotFoundError), + (208, "users not found. Specify owner.objectname (208)", NotFoundError), + (4002, "Login failed (4002)", DatabaseConnectionError), + (18456, "Login failed for user 'sa' (18456)", DatabaseConnectionError), + (20002, "Adaptive Server connection failed (20002)", DatabaseConnectionError), + (20047, "DBPROCESS is dead or not enabled (20047)", OperationalError), + ( + 2762, + "The 'CREATE TABLE' command is not allowed within a multi-statement transaction (2762)", + OperationalError, + ), + (1105, "Can't allocate space for object 'syslogs' in database 'pubs2' (1105)", OperationalError), + (247, "Arithmetic overflow during explicit conversion of INT value (247)", DataError), + (220, "Arithmetic overflow error for type tinyint (220)", DataError), + (168, "Scale error during implicit conversion of NUMERIC value (168)", DataError), + (3606, "Arithmetic overflow occurred (3606)", DataError), + (10330, "SELECT permission denied on object users, database pubs2, owner dbo (10330)", PermissionDeniedError), + (10331, "Permission denied, database pubs2, owner dbo (10331)", PermissionDeniedError), + (10332, "EXECUTE permission denied on object sp_proc (10332)", PermissionDeniedError), + ], +) +def test_create_mapped_exception_maps_sybase_ase_error_codes( + code: int, message: str, expected_type: type[Exception] +) -> None: + """Sybase ASE error codes map to their canonical SQLSpec exception classes.""" + mapped = create_mapped_exception(Exception(code, message)) + assert isinstance(mapped, expected_type) + + +def test_sybase_driver_profile_and_config_flags() -> None: + """SybaseConfig sets supports_transactional_ddl=False and registers the 'sybase' dialect.""" + assert SybaseConfig.supports_transactional_ddl is False + assert SybaseDriver.dialect == "sybase" + assert default_statement_config.dialect == "sybase" + assert driver_profile.default_execution_style is ParameterStyle.POSITIONAL_PYFORMAT + + +def test_sybase_driver_execute_many_script_and_transactions() -> None: + """SybaseDriver executes SELECT, DML, execute_many, execute_script, and transactions.""" + cursor = FakeCursor(rows=[(1, "Ada")], description=[("id",), ("name",)], rowcount=1) + connection = FakeConnection(cursor) + driver = SybaseDriver(cast("SybaseConnection", connection)) + + select_result = driver.execute("SELECT id, name FROM dbo.users WHERE id = ?", 1) + assert select_result.get_data() == [{"id": 1, "name": "Ada"}] + assert cursor.calls[-1] == ("SELECT id, name FROM dbo.users WHERE id = %s", (1,)) + + many_stmt = SQL( + "INSERT INTO dbo.users (id, name) VALUES (?, ?)", + [(1, "Ada"), (2, "Grace")], + statement_config=default_statement_config, + is_many=True, + ) + many_result = driver.dispatch_execute_many(cast("SybaseRawCursor", cursor), many_stmt) + assert many_result.rowcount_override == 1 + assert cursor.many_calls[-1] == ("INSERT INTO dbo.users (id, name) VALUES (%s, %s)", [(1, "Ada"), (2, "Grace")]) + + script_stmt = SQL( + "INSERT INTO dbo.users (id) VALUES (1); INSERT INTO dbo.users (id) VALUES (2);", + statement_config=default_statement_config, + is_script=True, + ) + script_result = driver.dispatch_execute_script(cast("SybaseRawCursor", cursor), script_stmt) + assert script_result.statement_count == 2 + assert script_result.successful_statements == 2 + + driver.begin() + assert driver._connection_in_transaction() is True + driver.commit() + assert driver._connection_in_transaction() is False + assert ("BEGIN TRANSACTION", None) in cursor.calls + assert ("IF @@TRANCOUNT > 0 COMMIT TRANSACTION", None) in cursor.calls + + with pytest.raises(TransactionError): + driver.create_savepoint("bad;name") + + +def test_sybase_driver_select_to_arrow_and_load_from_arrow() -> None: + """SybaseDriver converts SELECT results to Arrow and loads Arrow tables via executemany.""" + pa = pytest.importorskip("pyarrow") + cursor = FakeCursor(rows=[(1, "Ada"), (2, "Grace")], description=[("id",), ("name",)], rowcount=2) + connection = FakeConnection(cursor) + driver = SybaseDriver(cast("SybaseConnection", connection), driver_features=SybaseConfig().driver_features) + + arrow_result = driver.select_to_arrow("SELECT id, name FROM dbo.users") + assert arrow_result.rows_affected == 2 + + table = pa.table({"id": [1, 2], "name": ["Ada", "Grace"]}) + job = driver.load_from_arrow("dbo.users", table, overwrite=True) + assert job.telemetry["rows_processed"] == 2 + assert cursor.calls[-1] == ("DELETE FROM [dbo].[users]", None) + assert cursor.many_calls[-1] == ( + "INSERT INTO [dbo].[users] ([id], [name]) VALUES (%s, %s)", + [(1, "Ada"), (2, "Grace")], + ) + + +def test_sybase_data_dictionary_reflection() -> None: + """SybaseSyncDataDictionary parses ASE version strings and builds table/column/index/FK metadata.""" + assert SybaseDataDictionary is SybaseSyncDataDictionary + dd = SybaseSyncDataDictionary() + + class FakeDDDriver: + def select_one_or_none(self, _sql: Any, **_kwargs: Any) -> dict[str, Any]: + return {"version_string": "Adaptive Server Enterprise/16.0 SP04 PL04/EBF 30829 SMP/P/x86_64/Linux"} + + def select(self, _sql: Any, **kwargs: Any) -> list[dict[str, Any]]: + if kwargs.get("table_name") == "users": + return [ + { + "schema_name": "dbo", + "table_name": "users", + "column_name": "id", + "data_type": "numeric(10, 0)", + "is_nullable": False, + "column_default": None, + "ordinal_position": 1, + "max_length": 10, + "numeric_scale": 0, + "is_primary": True, + "is_unique": False, + "is_identity": True, + "identity_generation": "ALWAYS", + "is_generated": False, + }, + { + "schema_name": "dbo", + "table_name": "users", + "column_name": "bio", + "data_type": "unitext", + "is_nullable": True, + "column_default": None, + "ordinal_position": 2, + "max_length": 1073741823, + "numeric_scale": 0, + "is_primary": False, + "is_unique": False, + "is_identity": False, + "identity_generation": None, + "is_generated": False, + }, + ] + return [{"schema_name": "dbo", "table_name": "users", "table_type": "TABLE"}] + + fake_driver = cast("Any", FakeDDDriver()) + version = dd.get_version(fake_driver) + assert isinstance(version, SybaseVersionInfo) + assert (version.major, version.minor, version.patch) == (16, 0, 4) + assert dd.get_feature_flag(fake_driver, "supports_transactions") is True + assert dd.get_feature_flag(fake_driver, "supports_transactional_ddl") is False + assert dd.get_optimal_type(fake_driver, "timestamp") == "BIGDATETIME" + assert dd.get_optimal_type(fake_driver, "blob") == "IMAGE" + + tables = dd.get_tables(fake_driver, schema="dbo") + assert len(tables) == 1 + assert tables[0]["table_name"] == "users" + + columns = dd.get_columns(fake_driver, table="users", schema="dbo") + assert len(columns) == 2 + assert columns[0]["data_type"] == "numeric(10, 0)" + assert columns[0]["identity_generation"] == "ALWAYS" + assert columns[1]["data_type"] == "unitext" + + +def test_sybase_migration_tracker_and_extension_stores() -> None: + """SybaseSyncMigrationTracker, SybaseStore, SybaseSyncEventQueueStore, and SybaseADKStore emit ASE-safe SQL.""" + tracker = SybaseSyncMigrationTracker(version_table_name="dbo.ddl_migrations") + ddl_text = tracker._idempotent_tracking_table_ddl_text() + assert "IF OBJECT_ID(" in ddl_text + assert "EXECUTE(" in ddl_text + assert "VARCHAR(32)" in ddl_text + assert "BIGDATETIME" in ddl_text + assert "GETUTCDATE()" in ddl_text + assert "TEXT" in ddl_text + assert "NVARCHAR(MAX)" not in ddl_text + + config = SybaseConfig( + extension_config={ + "litestar": {"session_table": "litestar_session", "manage_schema": False}, + "events": {"queue_table": "event_queue"}, + "adk": {}, + } + ) + + litestar_store = SybaseStore(config) + litestar_ddl = litestar_store._table_ddl() + assert "IF OBJECT_ID(" in litestar_ddl + assert "IMAGE NOT NULL" in litestar_ddl + assert "BIGDATETIME" in litestar_ddl + assert "GETUTCDATE()" in litestar_ddl + + assert SybaseEventQueueStore is SybaseSyncEventQueueStore + event_store = SybaseSyncEventQueueStore(config) + event_stmts = event_store.create_statements() + assert "TEXT" in event_stmts[0] + assert "BIGDATETIME" in event_stmts[0] + assert "GETUTCDATE()" in event_stmts[0] + assert "IF OBJECT_ID(" in event_stmts[0] + + adk_store = SybaseADKStore(config) + sessions_ddl = adk_store._sessions_table_ddl() + events_ddl = adk_store._events_table_ddl() + assert "IF OBJECT_ID(" in sessions_ddl + assert "TEXT NOT NULL" in sessions_ddl + assert "BIGDATETIME" in sessions_ddl + assert "GETUTCDATE()" in sessions_ddl + assert "IF OBJECT_ID(" in events_ddl + + mem_store = SybaseADKMemoryStore(config) + mem_ddl = mem_store._memory_table_ddl() + assert "IF OBJECT_ID(" in mem_ddl + assert "TEXT NOT NULL" in mem_ddl + assert "BIGDATETIME" in mem_ddl + assert "GETUTCDATE()" in mem_ddl + + assert SybaseCursor.__slots__ == ("_as_dict", "connection", "cursor") + assert SybaseExceptionHandler.__slots__ == () + assert isinstance(SybaseADKConfig(), dict) + + +@pytest.mark.anyio +async def test_sybase_litestar_store_async_bridge(monkeypatch: pytest.MonkeyPatch) -> None: + """SybaseStore async methods bridge to sync execution via async_.""" + calls: list[str] = [] + monkeypatch.setattr(SybaseStore, "_create_table", lambda _self: calls.append("created")) + monkeypatch.setattr(SybaseStore, "reconcile_schema", AsyncMock()) + store = SybaseStore(SybaseConfig(extension_config={"litestar": {"session_table": "litestar_session"}})) + + await store.create_table() + assert calls == ["created"] + + +def test_sybase_adk_store_session_list_and_memory_operations(monkeypatch: pytest.MonkeyPatch) -> None: + """SybaseADKStore uses ROWS LIMIT ... OFFSET ... for pagination and SybaseADKMemoryStore inserts entries.""" + captured: list[tuple[str, tuple[Any, ...]]] = [] + + def _capture(_self: SybaseADKStore, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + captured.append((sql, tuple(params))) + return [] + + monkeypatch.setattr(SybaseADKStore, "_execute_fetchall", _capture) + config = MagicMock() + config.extension_config = {"adk": {}} + store = SybaseADKStore(cast("Any", config)) + + store.list_sessions("app", "u1", order_by="create_time", descending=False, limit=10, offset=20) + sql, params = captured[0] + normalized_sql = " ".join(sql.split()) + assert "ORDER BY create_time ASC, id ASC ROWS LIMIT %s OFFSET %s" in normalized_sql + assert params == ("app", "u1", 10, 20) + + conn = config.provide_connection.return_value.__enter__.return_value + cursor = conn.cursor.return_value + cursor.rowcount = 1 + mem_store = SybaseADKMemoryStore(cast("Any", config)) + now = datetime.now(tz=UTC) + entry = cast( + "Any", + { + "id": "mem-1", + "session_id": "sess-1", + "app_name": "app", + "user_id": "user-1", + "event_id": "evt-1", + "timestamp": now, + "content_json": {"text": "hello"}, + "content_text": "hello", + "metadata_json": None, + "inserted_at": now, + }, + ) + inserted = mem_store.insert_memory_entries([entry]) + assert inserted == 1 From 4f24e42f78989c1d972097bb571a893277d45818 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 18:25:30 +0000 Subject: [PATCH 27/35] feat(sqlanywhere): add sap sql anywhere adapter over sqlanydb and extension stores --- sqlspec/adapters/sqlanywhere/__init__.py | 40 + sqlspec/adapters/sqlanywhere/_typing.py | 119 +++ sqlspec/adapters/sqlanywhere/adk/__init__.py | 5 + sqlspec/adapters/sqlanywhere/adk/store.py | 875 ++++++++++++++++++ sqlspec/adapters/sqlanywhere/config.py | 205 ++++ sqlspec/adapters/sqlanywhere/core.py | 403 ++++++++ .../adapters/sqlanywhere/data_dictionary.py | 279 ++++++ sqlspec/adapters/sqlanywhere/driver.py | 354 +++++++ .../adapters/sqlanywhere/events/__init__.py | 5 + sqlspec/adapters/sqlanywhere/events/store.py | 31 + .../adapters/sqlanywhere/litestar/__init__.py | 5 + .../adapters/sqlanywhere/litestar/store.py | 215 +++++ sqlspec/adapters/sqlanywhere/migrations.py | 168 ++++ sqlspec/adapters/sqlanywhere/pool.py | 237 +++++ tests/unit/adapters/test_sqlanywhere.py | 489 ++++++++++ 15 files changed, 3430 insertions(+) create mode 100644 sqlspec/adapters/sqlanywhere/__init__.py create mode 100644 sqlspec/adapters/sqlanywhere/_typing.py create mode 100644 sqlspec/adapters/sqlanywhere/adk/__init__.py create mode 100644 sqlspec/adapters/sqlanywhere/adk/store.py create mode 100644 sqlspec/adapters/sqlanywhere/config.py create mode 100644 sqlspec/adapters/sqlanywhere/core.py create mode 100644 sqlspec/adapters/sqlanywhere/data_dictionary.py create mode 100644 sqlspec/adapters/sqlanywhere/driver.py create mode 100644 sqlspec/adapters/sqlanywhere/events/__init__.py create mode 100644 sqlspec/adapters/sqlanywhere/events/store.py create mode 100644 sqlspec/adapters/sqlanywhere/litestar/__init__.py create mode 100644 sqlspec/adapters/sqlanywhere/litestar/store.py create mode 100644 sqlspec/adapters/sqlanywhere/migrations.py create mode 100644 sqlspec/adapters/sqlanywhere/pool.py create mode 100644 tests/unit/adapters/test_sqlanywhere.py diff --git a/sqlspec/adapters/sqlanywhere/__init__.py b/sqlspec/adapters/sqlanywhere/__init__.py new file mode 100644 index 000000000..cdc65e606 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/__init__.py @@ -0,0 +1,40 @@ +"""SAP SQL Anywhere adapter for SQLSpec.""" + +from sqlspec.adapters.sqlanywhere._typing import SqlanywhereConnection, SqlanywhereCursor +from sqlspec.adapters.sqlanywhere.config import ( + SqlanywhereConfig, + SqlanywhereConnectionParams, + SqlanywhereDriverFeatures, + SqlanywherePoolParams, +) +from sqlspec.adapters.sqlanywhere.core import ( + DEFAULT_SQLANYWHERE_PORT, + build_connection_config, + create_mapped_exception, + default_statement_config, + driver_profile, +) +from sqlspec.adapters.sqlanywhere.data_dictionary import SqlanywhereDataDictionary, SqlanywhereSyncDataDictionary +from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver, SqlanywhereExceptionHandler +from sqlspec.adapters.sqlanywhere.migrations import SqlanywhereSyncMigrationTracker +from sqlspec.adapters.sqlanywhere.pool import SqlanywhereConnectionPool + +__all__ = ( + "DEFAULT_SQLANYWHERE_PORT", + "SqlanywhereConfig", + "SqlanywhereConnection", + "SqlanywhereConnectionParams", + "SqlanywhereConnectionPool", + "SqlanywhereCursor", + "SqlanywhereDataDictionary", + "SqlanywhereDriver", + "SqlanywhereDriverFeatures", + "SqlanywhereExceptionHandler", + "SqlanywherePoolParams", + "SqlanywhereSyncDataDictionary", + "SqlanywhereSyncMigrationTracker", + "build_connection_config", + "create_mapped_exception", + "default_statement_config", + "driver_profile", +) diff --git a/sqlspec/adapters/sqlanywhere/_typing.py b/sqlspec/adapters/sqlanywhere/_typing.py new file mode 100644 index 000000000..11e8b6c35 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/_typing.py @@ -0,0 +1,119 @@ +"""SAP SQL Anywhere adapter type definitions. + +This module contains type aliases and classes that are excluded from mypyc +compilation to avoid ABI boundary issues. +""" + +import contextlib +from typing import TYPE_CHECKING, Any + +from mypy_extensions import mypyc_attr + +from sqlspec.utils.module_loader import import_optional, import_optional_attr + +SQLANYDB_MODULE = import_optional("sqlanydb") +_DRIVER_CLS: Any = None + + +class _SqlanywhereFallbackError(Exception): + """Fallback exception base when sqlanydb is not installed.""" + + +if TYPE_CHECKING: + from collections.abc import Callable + from types import TracebackType + from typing import TypeAlias + + from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver + from sqlspec.core import StatementConfig + + SqlanywhereConnection: TypeAlias = Any + SqlanywhereRawCursor: TypeAlias = Any + SqlanywhereQueryParams: TypeAlias = Any + SqlanywhereError: TypeAlias = Exception + +if not TYPE_CHECKING: + SqlanywhereConnection = import_optional_attr("sqlanydb", "Connection") or Any + SqlanywhereRawCursor = import_optional_attr("sqlanydb", "Cursor") or Any + SqlanywhereQueryParams = Any + SqlanywhereError = import_optional_attr("sqlanydb", "Error") or _SqlanywhereFallbackError + +__all__ = ( + "SQLANYDB_MODULE", + "SqlanywhereConnection", + "SqlanywhereCursor", + "SqlanywhereError", + "SqlanywhereQueryParams", + "SqlanywhereRawCursor", + "SqlanywhereSessionContext", +) + + +class SqlanywhereCursor: + """Context manager for SQL Anywhere sqlanydb cursor operations.""" + + __slots__ = ("connection", "cursor") + + def __init__(self, connection: "SqlanywhereConnection") -> None: + self.connection = connection + self.cursor: SqlanywhereRawCursor | None = None + + def __enter__(self) -> "SqlanywhereRawCursor": + self.cursor = self.connection.cursor() + assert self.cursor is not None + return self.cursor + + def __exit__(self, *_: Any) -> None: + if self.cursor is not None and hasattr(self.cursor, "close"): + with contextlib.suppress(Exception): + self.cursor.close() + + +@mypyc_attr(allow_interpreted_subclasses=True, native_class=False) +class SqlanywhereSessionContext: + """Sync context manager for SAP SQL Anywhere sessions.""" + + __slots__ = ( + "_acquire_connection", + "_connection", + "_driver", + "_driver_features", + "_prepare_driver", + "_release_connection", + "_statement_config", + ) + + def __init__( + self, + acquire_connection: "Callable[[], Any]", + release_connection: "Callable[..., Any]", + statement_config: "StatementConfig", + driver_features: "dict[str, Any]", + prepare_driver: "Callable[[SqlanywhereDriver], SqlanywhereDriver]", + ) -> None: + self._acquire_connection = acquire_connection + self._release_connection = release_connection + self._statement_config = statement_config + self._driver_features = driver_features + self._prepare_driver = prepare_driver + self._connection: Any = None + self._driver: SqlanywhereDriver | None = None + + def __enter__(self) -> "SqlanywhereDriver": + global _DRIVER_CLS + if _DRIVER_CLS is None: + _DRIVER_CLS = import_optional_attr("sqlspec.adapters.sqlanywhere.driver", "SqlanywhereDriver") + self._connection = self._acquire_connection() + self._driver = _DRIVER_CLS( + connection=self._connection, statement_config=self._statement_config, driver_features=self._driver_features + ) + assert self._driver is not None + return self._prepare_driver(self._driver) + + def __exit__( + self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None" + ) -> "bool | None": + if self._connection is not None: + self._release_connection(self._connection) + self._connection = None + return None diff --git a/sqlspec/adapters/sqlanywhere/adk/__init__.py b/sqlspec/adapters/sqlanywhere/adk/__init__.py new file mode 100644 index 000000000..5b4598be7 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/adk/__init__.py @@ -0,0 +1,5 @@ +"""SAP SQL Anywhere ADK store module.""" + +from sqlspec.adapters.sqlanywhere.adk.store import SqlanywhereADKConfig, SqlanywhereADKMemoryStore, SqlanywhereADKStore + +__all__ = ("SqlanywhereADKConfig", "SqlanywhereADKMemoryStore", "SqlanywhereADKStore") diff --git a/sqlspec/adapters/sqlanywhere/adk/store.py b/sqlspec/adapters/sqlanywhere/adk/store.py new file mode 100644 index 000000000..90f00548e --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/adk/store.py @@ -0,0 +1,875 @@ +"""SAP SQL Anywhere ADK stores for Google Agent Development Kit session storage.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, cast + +from sqlspec.adapters.sqlanywhere._typing import SqlanywhereCursor, SqlanywhereError +from sqlspec.adapters.sqlanywhere.core import ( + extract_sqlstate_and_sqlcode, + quote_double_identifier, + resolve_rowcount, + split_schema_and_table, +) +from sqlspec.config import ADKConfig +from sqlspec.extensions.adk import BaseSyncADKStore, StoredEvent, StoredSession, normalize_session_list_options +from sqlspec.extensions.adk.memory.store import BaseSyncADKMemoryStore +from sqlspec.utils.serializers import from_json, to_json + +if TYPE_CHECKING: + from collections.abc import Sequence + from datetime import timedelta + + from sqlspec.adapters.sqlanywhere.config import SqlanywhereConfig + from sqlspec.extensions.adk import SessionOrderBy + from sqlspec.extensions.adk.memory._types import StoredMemory + +__all__ = ("SqlanywhereADKConfig", "SqlanywhereADKMemoryStore", "SqlanywhereADKStore") + +SQLANYWHERE_TABLE_NOT_FOUND_SQLCODE: Final[int] = -141 +JSON_COLUMN_TYPE: Final[str] = "LONG VARCHAR" + + +class SqlanywhereADKConfig(ADKConfig): + """SAP SQL Anywhere ADK extension settings.""" + + +class SqlanywhereADKStore(BaseSyncADKStore["SqlanywhereConfig"]): + """Synchronous SAP SQL Anywhere ADK session/event store.""" + + connector_name: ClassVar[str] = "sqlanywhere" + __slots__ = () + + def __init__(self, config: "SqlanywhereConfig") -> None: + super().__init__(config) + + def create_tables(self) -> None: + """Create ADK tables and indexes using SQL Anywhere ``IF NOT EXISTS`` DDL.""" + if not self.create_schema_enabled: + self.reconcile_schema() + return + + with self._config.provide_session() as driver: + driver.execute_script(self._sessions_table_ddl()) + driver.execute_script(self._events_table_ddl()) + driver.execute_script(self._app_states_table_ddl()) + driver.execute_script(self._user_states_table_ddl()) + driver.execute_script(self._metadata_table_ddl()) + for index_name, index_table, columns in self._index_specs(): + driver.execute(_create_index_sql(index_table, index_name, columns)) + driver.commit() + self.reconcile_schema(assume_existing=True) + + def create_session( + self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None + ) -> StoredSession: + """Create a new ADK session.""" + owner_column = f", {quote_double_identifier(self._owner_id_column_name)}" if self._owner_id_column_name else "" + owner_param = ", ?" if self._owner_id_column_name else "" + insert_sql = f""" + INSERT INTO {_table_ref(self._session_table)} ( + id, app_name, user_id{owner_column}, state, create_time, update_time + ) + VALUES (?, ?, ?{owner_param}, ?, CURRENT UTC TIMESTAMP, CURRENT UTC TIMESTAMP) + """ + select_sql = f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = ? AND user_id = ? AND id = ? + """ + params: tuple[Any, ...] + if self._owner_id_column_name: + params = (session_id, app_name, user_id, owner_id, to_json(state)) + else: + params = (session_id, app_name, user_id, to_json(state)) + self._execute(insert_sql, params, commit=True) + row = self._execute_fetchone(select_sql, (app_name, user_id, session_id)) + if row is None: + msg = "Failed to fetch created session" + raise RuntimeError(msg) + return _session_record_from_row(row) + + def get_session( + self, app_name: str, user_id: str, session_id: str, *, renew_for: "int | timedelta | None" = None + ) -> "StoredSession | None": + """Return a scoped session or ``None`` if absent.""" + try: + if renew_for is not None and self._calculate_expires_at(renew_for) is not None: + self._execute( + f""" + UPDATE {_table_ref(self._session_table)} + SET update_time = CURRENT UTC TIMESTAMP + WHERE app_name = ? AND user_id = ? AND id = ? + """, + (app_name, user_id, session_id), + commit=True, + ) + row = self._execute_fetchone( + f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = ? AND user_id = ? AND id = ? + """, + (app_name, user_id, session_id), + ) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return None + raise + return _session_record_from_row(row) if row is not None else None + + def update_session_state(self, app_name: str, user_id: str, session_id: str, state: "dict[str, Any]") -> None: + """Replace a session's durable state.""" + self._execute( + f""" + UPDATE {_table_ref(self._session_table)} + SET state = ?, update_time = CURRENT UTC TIMESTAMP + WHERE app_name = ? AND user_id = ? AND id = ? + """, + (to_json(state), app_name, user_id, session_id), + commit=True, + ) + + def list_sessions( + self, + app_name: str, + user_id: "str | None" = None, + *, + order_by: "SessionOrderBy" = "update_time", + descending: bool = True, + limit: "int | None" = None, + offset: "int | None" = None, + ) -> "list[StoredSession]": + """List ADK sessions for an application, optionally scoped to a user.""" + column, direction, page_limit, page_offset = normalize_session_list_options(order_by, descending, limit, offset) + if page_limit == 0: + return [] + + sql, params = _session_list_query( + self._session_table, app_name, user_id, column, direction, page_limit, page_offset + ) + try: + rows = self._execute_fetchall(sql, params) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return [] + raise + return [_session_record_from_row(row) for row in rows] + + def delete_session(self, app_name: str, user_id: str, session_id: str) -> None: + """Delete a session and its associated events.""" + self._execute( + f"DELETE FROM {_table_ref(self._events_table)} WHERE app_name = ? AND user_id = ? AND session_id = ?", + (app_name, user_id, session_id), + commit=True, + ) + self._execute( + f"DELETE FROM {_table_ref(self._session_table)} WHERE app_name = ? AND user_id = ? AND id = ?", + (app_name, user_id, session_id), + commit=True, + ) + + def append_event(self, event_record: StoredEvent) -> None: + """Append an event to a session.""" + self._execute(_insert_event_sql(self._events_table), _event_insert_params(event_record), commit=True) + + def append_event_and_update_state( + self, + event_record: StoredEvent, + app_name: str, + user_id: str, + session_id: str, + state: "dict[str, Any]", + *, + app_state: "dict[str, Any] | None" = None, + user_state: "dict[str, Any] | None" = None, + ) -> StoredSession: + """Atomically append an event and update durable session/scoped state.""" + update_sql = f""" + UPDATE {_table_ref(self._session_table)} + SET state = ?, update_time = CURRENT UTC TIMESTAMP + WHERE app_name = ? AND user_id = ? AND id = ? + """ + select_sql = f""" + SELECT TOP 1 id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(self._session_table)} + WHERE app_name = ? AND user_id = ? AND id = ? + """ + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + try: + cursor.execute(update_sql, (to_json(state), app_name, user_id, session_id)) + cursor.execute(select_sql, (app_name, user_id, session_id)) + row = cursor.fetchone() + if row is None: + _raise_session_not_found(session_id) + cursor.execute(_insert_event_sql(self._events_table), _event_insert_params(event_record)) + if app_state is not None: + app_json = to_json(app_state) + cursor.execute(self._upsert_app_state_sql(), (app_name, app_json)) + if user_state is not None: + user_json = to_json(user_state) + cursor.execute(self._upsert_user_state_sql(), (app_name, user_id, user_json)) + except Exception: + conn.rollback() + raise + conn.commit() + return _session_record_from_row(row) + + def get_events( + self, + app_name: str, + user_id: str, + session_id: str, + after_timestamp: "datetime | None" = None, + limit: "int | None" = None, + ) -> "list[StoredEvent]": + """Return events for a scoped session ordered by event timestamp.""" + if limit == 0: + return [] + sql, params = self._events_query(app_name, user_id, session_id, after_timestamp, limit) + try: + rows = self._execute_fetchall(sql, params) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return [] + raise + return [_event_record_from_row(row) for row in rows] + + def delete_expired_events(self, before: datetime, app_name: "str | None" = None) -> int: + """Delete events older than ``before``.""" + sql = f"DELETE FROM {_table_ref(self._events_table)} WHERE timestamp < ?" + params: list[Any] = [before] + if app_name is not None: + sql += " AND app_name = ?" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return 0 + raise + + def delete_idle_sessions(self, updated_before: datetime, app_name: "str | None" = None) -> int: + """Delete sessions whose update_time is older than ``updated_before``.""" + sql = f"DELETE FROM {_table_ref(self._session_table)} WHERE update_time < ?" + params: list[Any] = [updated_before] + if app_name is not None: + sql += " AND app_name = ?" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return 0 + raise + + def delete_idle_user_states(self, updated_before: datetime, app_name: "str | None" = None) -> int: + """Delete user state rows whose update_time is older than ``updated_before``.""" + sql = f"DELETE FROM {_table_ref(self._user_state_table)} WHERE update_time < ?" + params: list[Any] = [updated_before] + if app_name is not None: + sql += " AND app_name = ?" + params.append(app_name) + try: + return self._execute(sql, tuple(params), commit=True) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return 0 + raise + + def get_app_state(self, app_name: str) -> "dict[str, Any] | None": + """Return app-scoped state.""" + try: + row = self._execute_fetchone( + f"SELECT TOP 1 state FROM {_table_ref(self._app_state_table)} WHERE app_name = ?", (app_name,) + ) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return None + raise + return _json_dict(_row_value(row, "state", 0)) if row is not None else None + + def get_user_state(self, app_name: str, user_id: str) -> "dict[str, Any] | None": + """Return user-scoped state.""" + try: + row = self._execute_fetchone( + f""" + SELECT TOP 1 state + FROM {_table_ref(self._user_state_table)} + WHERE app_name = ? AND user_id = ? + """, + (app_name, user_id), + ) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return None + raise + return _json_dict(_row_value(row, "state", 0)) if row is not None else None + + def upsert_app_state(self, app_name: str, state: "dict[str, Any]") -> None: + """Insert or replace app-scoped state.""" + encoded = to_json(state) + self._execute(self._upsert_app_state_sql(), (app_name, encoded), commit=True) + + def upsert_user_state(self, app_name: str, user_id: str, state: "dict[str, Any]") -> None: + """Insert or replace user-scoped state.""" + encoded = to_json(state) + self._execute(self._upsert_user_state_sql(), (app_name, user_id, encoded), commit=True) + + def get_metadata(self, key: str) -> "str | None": + """Return an ADK metadata value.""" + try: + row = self._execute_fetchone( + f'SELECT TOP 1 value FROM {_table_ref(self._metadata_table)} WHERE "key" = ?', (key,) + ) + except SqlanywhereError as exc: + if _is_sqlanywhere_table_missing(exc): + return None + raise + return str(_row_value(row, "value", 0)) if row is not None else None + + def set_metadata(self, key: str, value: str) -> None: + """Set an ADK metadata value.""" + self._execute(_upsert_metadata_sql(self._metadata_table), (key, value), commit=True) + + def _index_specs(self) -> "list[tuple[str, str, str]]": + """Return ``(index_name, table, columns)`` specs for session and event indexes.""" + return [*_sessions_index_specs(self._session_table), *_events_index_specs(self._events_table)] + + def _sessions_table_ddl(self) -> str: + """Return SQL Anywhere DDL for the ADK session table.""" + return _sessions_table_ddl(self._session_table, JSON_COLUMN_TYPE, self._owner_id_column_ddl) + + def _events_table_ddl(self) -> str: + """Return SQL Anywhere DDL for the ADK event table.""" + return _events_table_ddl(self._events_table, self._session_table, JSON_COLUMN_TYPE) + + def _app_states_table_ddl(self) -> str: + """Return SQL Anywhere DDL for the app-scoped state table.""" + return _app_states_table_ddl(self._app_state_table, JSON_COLUMN_TYPE) + + def _user_states_table_ddl(self) -> str: + """Return SQL Anywhere DDL for the user-scoped state table.""" + return _user_states_table_ddl(self._user_state_table, JSON_COLUMN_TYPE) + + def _metadata_table_ddl(self) -> str: + """Return SQL Anywhere DDL for the ADK metadata table.""" + return _metadata_table_ddl(self._metadata_table) + + def _drop_app_states_table_sql(self) -> str: + return f"DROP TABLE IF EXISTS {_table_ref(self._app_state_table)}" + + def _drop_user_states_table_sql(self) -> str: + return f"DROP TABLE IF EXISTS {_table_ref(self._user_state_table)}" + + def _drop_metadata_table_sql(self) -> str: + return f"DROP TABLE IF EXISTS {_table_ref(self._metadata_table)}" + + def _drop_tables_sql(self) -> "list[str]": + return [ + self._drop_metadata_table_sql(), + self._drop_user_states_table_sql(), + self._drop_app_states_table_sql(), + f"DROP TABLE IF EXISTS {_table_ref(self._events_table)}", + f"DROP TABLE IF EXISTS {_table_ref(self._session_table)}", + ] + + def _upsert_app_state_sql(self) -> str: + return _upsert_state_sql(self._app_state_table, ("app_name",)) + + def _upsert_user_state_sql(self) -> str: + return _upsert_state_sql(self._user_state_table, ("app_name", "user_id")) + + def _events_query( + self, + app_name: str, + user_id: str, + session_id: str, + after_timestamp: "datetime | None" = None, + limit: "int | None" = None, + ) -> "tuple[str, tuple[Any, ...]]": + return _events_query(self._events_table, app_name, user_id, session_id, after_timestamp, limit) + + def _execute_fetchone(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> "Any | None": + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, params) + row = cursor.fetchone() + if commit: + conn.commit() + return row + + def _execute_fetchall(self, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, params) + return list(cursor.fetchall()) + + def _execute(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> int: + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, params) + rowcount = resolve_rowcount(cursor) + if commit: + conn.commit() + return rowcount + + +class SqlanywhereADKMemoryStore(BaseSyncADKMemoryStore["SqlanywhereConfig"]): + """SAP SQL Anywhere ADK memory store using sqlanydb.""" + + __slots__ = () + + def __init__(self, config: "SqlanywhereConfig") -> None: + super().__init__(config) + + def create_tables(self) -> None: + """Create the memory table and indexes using SQL Anywhere ``IF NOT EXISTS`` DDL.""" + if not self.create_schema_enabled: + self.reconcile_schema() + return + + if not self._enabled: + return + with self._config.provide_session() as driver: + driver.execute_script(self._memory_table_ddl()) + for index_name, index_table, columns in self._memory_index_specs(): + driver.execute(_create_index_sql(index_table, index_name, columns)) + driver.commit() + self.reconcile_schema(assume_existing=True) + + def insert_memory_entries(self, entries: "list[StoredMemory]", owner_id: "object | None" = None) -> int: + """Bulk insert memory entries with event-id deduplication.""" + if not self._enabled: + msg = "ADK memory store is disabled" + raise RuntimeError(msg) + if not entries: + return 0 + + owner_column = f", {quote_double_identifier(self._owner_id_column_name)}" if self._owner_id_column_name else "" + owner_value = ", ?" if self._owner_id_column_name else "" + sql = f""" + INSERT INTO {_table_ref(self._memory_table)} ( + id, session_id, app_name, user_id, scope, event_id, author, timestamp, + content_json, content_text, metadata_json{owner_column} + ) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?{owner_value} + WHERE NOT EXISTS ( + SELECT 1 FROM {_table_ref(self._memory_table)} + WHERE event_id = ? + ) + """ + inserted = 0 + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + try: + for entry in entries: + params: tuple[Any, ...] = ( + entry["id"], + entry["session_id"], + entry["app_name"], + entry["user_id"], + entry.get("scope", "user"), + entry["event_id"], + entry.get("author"), + entry["timestamp"], + to_json(entry["content_json"]), + entry["content_text"], + to_json(entry["metadata_json"]) if entry.get("metadata_json") is not None else None, + ) + if self._owner_id_column_name: + params = (*params, owner_id) + cursor.execute(sql, (*params, entry["event_id"])) + inserted += resolve_rowcount(cursor) + except Exception: + conn.rollback() + raise + conn.commit() + return inserted + + def search_entries( + self, + query: str, + app_name: str, + user_id: str, + limit: "int | None" = None, + scope_filter: Literal["all", "user", "app"] = "all", + embedding: "Sequence[float] | None" = None, + ) -> "list[StoredMemory]": + """Search memory entries by text query.""" + _ = embedding + if not self._enabled: + msg = "ADK memory store is disabled" + raise RuntimeError(msg) + limit_value = limit or self._max_results + where_scope, scope_params = _build_sqlanywhere_scope_where(app_name, user_id, scope_filter) + escaped_query = _escape_like_pattern(query) + sql = f""" + SELECT + id, session_id, app_name, user_id, scope, event_id, author, timestamp, + content_json, content_text, metadata_json, inserted_at + FROM {_table_ref(self._memory_table)} + WHERE {where_scope} AND content_text LIKE ? ESCAPE '\\' + ORDER BY timestamp DESC + ROWS LIMIT ? + """ + rows = self._execute_fetchall(sql, (*scope_params, f"%{escaped_query}%", limit_value)) + return [_memory_record_from_row(row) for row in rows] + + def delete_entries_by_session(self, session_id: str) -> int: + """Delete all memory entries for a specific session.""" + return self._execute( + f"DELETE FROM {_table_ref(self._memory_table)} WHERE session_id = ?", (session_id,), commit=True + ) + + def delete_entries_older_than(self, days: int, app_name: "str | None" = None, scope: "str | None" = None) -> int: + """Delete memory entries older than the retention window.""" + clauses = ["inserted_at < DATEADD(day, -?, CURRENT UTC TIMESTAMP)"] + params: list[Any] = [days] + if app_name is not None: + clauses.append("app_name = ?") + params.append(app_name) + if scope is not None: + clauses.append("scope = ?") + params.append(scope) + where_sql = " AND ".join(clauses) + return self._execute( + f"DELETE FROM {_table_ref(self._memory_table)} WHERE {where_sql}", tuple(params), commit=True + ) + + def _memory_table_ddl(self) -> str: + owner_line = f", {self._owner_id_column_ddl}" if self._owner_id_column_ddl else "" + table_ref = _table_ref(self._memory_table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "session_id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + "scope VARCHAR(16) DEFAULT 'user' NOT NULL, " + "event_id VARCHAR(128) NOT NULL, " + "author VARCHAR(256) NULL, " + "timestamp TIMESTAMP NOT NULL, " + f"content_json {JSON_COLUMN_TYPE} NOT NULL, " + "content_text LONG VARCHAR NOT NULL, " + f"metadata_json {JSON_COLUMN_TYPE} NULL, " + f"inserted_at TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL{owner_line}, " + f"CONSTRAINT {_constraint_ref('pk', self._memory_table, 'id')} PRIMARY KEY (id), " + f"CONSTRAINT {_constraint_ref('uq', self._memory_table, 'event_id')} UNIQUE (event_id)" + ");" + ) + + def _memory_index_specs(self) -> "list[tuple[str, str, str]]": + """Return ``(index_name, table, columns)`` specs for memory-table indexes.""" + _, table_name = split_schema_and_table(self._memory_table) + return [ + (f"idx_{table_name}_app_scope_user_time", self._memory_table, "app_name, scope, user_id, timestamp DESC"), + (f"idx_{table_name}_scope", self._memory_table, "app_name, scope"), + (f"idx_{table_name}_session", self._memory_table, "session_id"), + (f"idx_{table_name}_timestamp", self._memory_table, "timestamp DESC"), + ] + + def _drop_memory_table_sql(self) -> "list[str]": + return [f"DROP TABLE IF EXISTS {_table_ref(self._memory_table)}"] + + def _execute_fetchall(self, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, params) + return list(cursor.fetchall()) + + def _execute(self, sql: str, params: "tuple[Any, ...]" = (), *, commit: bool = False) -> int: + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, params) + rowcount = resolve_rowcount(cursor) + if commit: + conn.commit() + return rowcount + + +def _sessions_table_ddl(table: str, json_column_type: str, owner_id_column_ddl: "str | None") -> str: + owner_line = f", {owner_id_column_ddl}" if owner_id_column_ddl else "" + table_ref = _table_ref(table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + f"user_id VARCHAR(128) NOT NULL{owner_line}, " + f"state {json_column_type} NOT NULL, " + "create_time TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL, " + "update_time TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'id')} PRIMARY KEY (id)" + ");" + ) + + +def _sessions_index_specs(table: str) -> "list[tuple[str, str, str]]": + _, table_name = split_schema_and_table(table) + return [ + (f"idx_{table_name}_app_user", table, "app_name, user_id"), + (f"idx_{table_name}_update_time", table, "update_time DESC"), + ] + + +def _events_table_ddl(table: str, session_table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + session_ref = _table_ref(session_table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "id VARCHAR(128) NOT NULL, " + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + "session_id VARCHAR(128) NOT NULL, " + "invocation_id VARCHAR(256) NOT NULL, " + "timestamp TIMESTAMP NOT NULL, " + f"event_data {json_column_type} NOT NULL, " + "actions LONG BINARY NULL, " + "partial BIT NULL, " + "turn_complete BIT NULL, " + "interrupted BIT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'id')} PRIMARY KEY (id), " + f"CONSTRAINT {_constraint_ref('fk', table, 'session')} FOREIGN KEY (session_id) REFERENCES {session_ref}(id)" + ");" + ) + + +def _events_index_specs(table: str) -> "list[tuple[str, str, str]]": + _, table_name = split_schema_and_table(table) + return [ + (f"idx_{table_name}_scope", table, "app_name, user_id, session_id, timestamp ASC"), + (f"idx_{table_name}_session", table, "session_id, timestamp ASC"), + (f"idx_{table_name}_invocation", table, "invocation_id"), + (f"idx_{table_name}_timestamp", table, "timestamp ASC"), + (f"idx_{table_name}_app_timestamp", table, "app_name, timestamp ASC"), + ] + + +def _app_states_table_ddl(table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "app_name VARCHAR(128) NOT NULL, " + f"state {json_column_type} NOT NULL, " + "update_time TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'app_name')} PRIMARY KEY (app_name)" + ");" + ) + + +def _user_states_table_ddl(table: str, json_column_type: str) -> str: + table_ref = _table_ref(table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "app_name VARCHAR(128) NOT NULL, " + "user_id VARCHAR(128) NOT NULL, " + f"state {json_column_type} NOT NULL, " + "update_time TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL, " + f"CONSTRAINT {_constraint_ref('pk', table, 'app_user')} PRIMARY KEY (app_name, user_id)" + ");" + ) + + +def _metadata_table_ddl(table: str) -> str: + table_ref = _table_ref(table) + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + '"key" VARCHAR(128) NOT NULL, ' + "value VARCHAR(512) NOT NULL, " + f'CONSTRAINT {_constraint_ref("pk", table, "key")} PRIMARY KEY ("key")' + ");" + ) + + +def _create_index_sql(table: str, index_name: str, columns: str) -> str: + return f"CREATE INDEX IF NOT EXISTS {quote_double_identifier(index_name)} ON {_table_ref(table)} ({columns})" + + +def _insert_event_sql(table: str) -> str: + return f""" + INSERT INTO {_table_ref(table)} ( + id, app_name, user_id, session_id, invocation_id, timestamp, event_data + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """ + + +def _upsert_state_sql(table: str, key_columns: "tuple[str, ...]") -> str: + table_ref = _table_ref(table) + insert_columns = ", ".join(quote_double_identifier(col) for col in (*key_columns, "state", "update_time")) + insert_placeholders = ", ".join(["?" for _ in key_columns] + ["?", "CURRENT UTC TIMESTAMP"]) + return f""" + INSERT INTO {table_ref} ({insert_columns}) + ON EXISTING UPDATE + VALUES ({insert_placeholders}) + """ + + +def _upsert_metadata_sql(table: str) -> str: + table_ref = _table_ref(table) + return f""" + INSERT INTO {table_ref} ("key", value) + ON EXISTING UPDATE + VALUES (?, ?) + """ + + +def _events_query( + table: str, app_name: str, user_id: str, session_id: str, after_timestamp: "datetime | None", limit: "int | None" +) -> "tuple[str, tuple[Any, ...]]": + params: list[Any] = [app_name, user_id, session_id] + after_clause = "" + if after_timestamp is not None: + after_clause = " AND timestamp > ?" + params.append(after_timestamp) + limit_clause = "" + if limit is not None: + limit_clause = "\n ROWS LIMIT ?" + params.append(limit) + sql = f""" + SELECT id, app_name, user_id, session_id, invocation_id, timestamp, event_data + FROM {_table_ref(table)} + WHERE app_name = ? AND user_id = ? AND session_id = ?{after_clause} + ORDER BY timestamp ASC{limit_clause} + """ + return sql, tuple(params) + + +def _event_insert_params(event_record: StoredEvent) -> "tuple[Any, ...]": + return ( + event_record["id"], + event_record["app_name"], + event_record["user_id"], + event_record["session_id"], + event_record["invocation_id"], + event_record["timestamp"], + to_json(event_record["event_data"]), + ) + + +def _row_value(row: object, key: str, index: int) -> Any: + if isinstance(row, dict): + if key in row: + return row[key] + upper_key = key.upper() + if upper_key in row: + return row[upper_key] + return None + if isinstance(row, (list, tuple)) and len(row) > index: + return row[index] + return getattr(row, key, None) + + +def _session_record_from_row(row: Any) -> StoredSession: + return StoredSession( + id=_row_value(row, "id", 0), + app_name=_row_value(row, "app_name", 1), + user_id=_row_value(row, "user_id", 2), + state=_json_dict(_row_value(row, "state", 3)), + create_time=_row_value(row, "create_time", 4), + update_time=_row_value(row, "update_time", 5), + ) + + +def _event_record_from_row(row: Any) -> StoredEvent: + return StoredEvent( + id=_row_value(row, "id", 0), + app_name=_row_value(row, "app_name", 1), + user_id=_row_value(row, "user_id", 2), + session_id=_row_value(row, "session_id", 3), + invocation_id=_row_value(row, "invocation_id", 4), + timestamp=_row_value(row, "timestamp", 5), + event_data=_json_dict(_row_value(row, "event_data", 6)), + ) + + +def _memory_record_from_row(row: Any) -> "StoredMemory": + metadata_raw = _row_value(row, "metadata_json", 10) + return cast( + "StoredMemory", + { + "id": _row_value(row, "id", 0), + "session_id": _row_value(row, "session_id", 1), + "app_name": _row_value(row, "app_name", 2), + "user_id": _row_value(row, "user_id", 3), + "scope": _row_value(row, "scope", 4), + "event_id": _row_value(row, "event_id", 5), + "author": _row_value(row, "author", 6), + "timestamp": _row_value(row, "timestamp", 7), + "content_json": _json_dict(_row_value(row, "content_json", 8)), + "content_text": _row_value(row, "content_text", 9), + "metadata_json": _json_dict(metadata_raw) if metadata_raw is not None else None, + "inserted_at": _row_value(row, "inserted_at", 11), + "embedding": None, + }, + ) + + +def _json_dict(value: Any) -> "dict[str, Any]": + if value is None: + return {} + if isinstance(value, dict): + return cast("dict[str, Any]", value) + if isinstance(value, bytearray): + value = bytes(value) + if isinstance(value, (bytes, str)): + return cast("dict[str, Any]", from_json(value)) + return cast("dict[str, Any]", from_json(str(value))) + + +def _is_sqlanywhere_table_missing(exc: BaseException) -> bool: + text = str(exc).lower() + _, sqlcode = extract_sqlstate_and_sqlcode(exc) + return "not found" in text or sqlcode == SQLANYWHERE_TABLE_NOT_FOUND_SQLCODE + + +def _table_ref(table: str) -> str: + schema_name, table_name = split_schema_and_table(table) + return f"{quote_double_identifier(schema_name)}.{quote_double_identifier(table_name)}" + + +def _constraint_ref(prefix: str, table: str, suffix: str) -> str: + _, table_name = split_schema_and_table(table) + return quote_double_identifier(f"{prefix}_{table_name}_{suffix}") + + +def _raise_session_not_found(session_id: str) -> None: + msg = f"Session {session_id} not found during append_event_and_update_state." + raise ValueError(msg) + + +def _build_sqlanywhere_scope_where( + app_name: str, user_id: str, scope_filter: Literal["all", "user", "app"] +) -> tuple[str, tuple[Any, ...]]: + if scope_filter == "all": + return "app_name = ? AND ((scope = 'user' AND user_id = ?) OR scope = 'app')", (app_name, user_id) + if scope_filter == "user": + return "app_name = ? AND scope = 'user' AND user_id = ?", (app_name, user_id) + return "app_name = ? AND scope = 'app'", (app_name,) + + +def _escape_like_pattern(value: str) -> str: + return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + + +def _session_list_query( + session_table: str, + app_name: str, + user_id: "str | None", + column: str, + direction: str, + limit: "int | None", + offset: int, +) -> "tuple[str, tuple[Any, ...]]": + """Return the bounded SQL Anywhere session-list query and its bound values.""" + params: list[Any] = [app_name] + where_clause = "app_name = ?" + if user_id is not None: + params.append(user_id) + where_clause = f"{where_clause} AND user_id = ?" + + page_clause = "" + if limit is not None: + params.extend((limit, offset)) + page_clause = "\n ROWS LIMIT ? OFFSET ?" + + sql = f""" + SELECT id, app_name, user_id, state, create_time, update_time + FROM {_table_ref(session_table)} + WHERE {where_clause} + ORDER BY {column} {direction}, id {direction}{page_clause} + """ + return sql, tuple(params) diff --git a/sqlspec/adapters/sqlanywhere/config.py b/sqlspec/adapters/sqlanywhere/config.py new file mode 100644 index 000000000..04c159a5e --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/config.py @@ -0,0 +1,205 @@ +"""SAP SQL Anywhere database configuration.""" + +from typing import TYPE_CHECKING, Any, ClassVar, Literal, NotRequired, TypedDict, cast + +from sqlspec.adapters.sqlanywhere._typing import ( + SqlanywhereConnection, + SqlanywhereCursor, + SqlanywhereRawCursor, + SqlanywhereSessionContext, +) +from sqlspec.adapters.sqlanywhere.core import apply_driver_features, build_connection_config, default_statement_config +from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver, SqlanywhereExceptionHandler +from sqlspec.adapters.sqlanywhere.migrations import SqlanywhereSyncMigrationTracker +from sqlspec.adapters.sqlanywhere.pool import SqlanywhereConnectionPool +from sqlspec.config import ExtensionConfigs, SyncDatabaseConfig +from sqlspec.core import TypeCoercionCapabilities +from sqlspec.driver import SyncPoolConnectionContext, SyncPoolSessionFactory +from sqlspec.extensions.events import EventRuntimeHints +from sqlspec.utils.config_tools import normalize_connection_config + +if TYPE_CHECKING: + from collections.abc import Callable + + from sqlspec.core import StatementConfig + from sqlspec.observability import ObservabilityConfig + +__all__ = ( + "SqlanywhereConfig", + "SqlanywhereConnectionContext", + "SqlanywhereConnectionParams", + "SqlanywhereDriverFeatures", + "SqlanywherePoolParams", +) + + +class SqlanywhereConnectionParams(TypedDict): + """SAP SQL Anywhere connection parameters.""" + + uid: NotRequired[str] + user: NotRequired[str] + username: NotRequired[str] + pwd: NotRequired[str] + password: NotRequired[str] + dbn: NotRequired[str] + database: NotRequired[str] + db: NotRequired[str] + eng: NotRequired[str] + server: NotRequired[str] + engine: NotRequired[str] + server_name: NotRequired[str] + dbf: NotRequired[str] + database_file: NotRequired[str] + host: NotRequired[str] + port: NotRequired[int | str] + dsn: NotRequired[str] + links: NotRequired[str] + cset: NotRequired[str] + extra: NotRequired["dict[str, Any]"] + + +class SqlanywherePoolParams(SqlanywhereConnectionParams): + """SAP SQL Anywhere pool parameters.""" + + pool_min_size: NotRequired[int] + pool_max_size: NotRequired[int] + pool_timeout: NotRequired[float] + pool_recycle_seconds: NotRequired[int] + health_check_interval: NotRequired[float] + + +class SqlanywhereDriverFeatures(TypedDict): + """SAP SQL Anywhere driver feature flags.""" + + json_serializer: NotRequired["Callable[[Any], str]"] + json_deserializer: NotRequired["Callable[[str], Any]"] + on_connection_create: "NotRequired[Callable[[SqlanywhereConnection], None]]" + enable_events: NotRequired[bool] + events_backend: NotRequired[Literal["poll_queue"]] + + +class SqlanywhereConnectionContext(SyncPoolConnectionContext): + """Context manager for SAP SQL Anywhere connections.""" + + __slots__ = () + + +class _SqlanywhereSessionConnectionHandler(SyncPoolSessionFactory): + __slots__ = () + + +class SqlanywhereConfig(SyncDatabaseConfig[SqlanywhereConnection, SqlanywhereConnectionPool, SqlanywhereDriver]): + """Configuration for SAP SQL Anywhere synchronous connections over sqlanydb.""" + + __slots__ = ( + "_health_check_interval", + "_pool_max_size", + "_pool_min_size", + "_pool_recycle_seconds", + "_pool_timeout", + "_user_connection_hook", + ) + + driver_type: "ClassVar[type[SqlanywhereDriver]]" = SqlanywhereDriver + connection_type: "ClassVar[type[SqlanywhereConnection]]" = cast( + "type[SqlanywhereConnection]", SqlanywhereConnection + ) + migration_tracker_type: "ClassVar[type[SqlanywhereSyncMigrationTracker]]" = SqlanywhereSyncMigrationTracker + supports_transactional_ddl: "ClassVar[bool]" = False + supports_migration_schemas: "ClassVar[bool]" = True + supports_native_arrow_export: "ClassVar[bool]" = True + supports_native_arrow_import: "ClassVar[bool]" = True + supports_native_parquet_export: "ClassVar[bool]" = True + supports_native_parquet_import: "ClassVar[bool]" = True + supports_native_row_streaming: "ClassVar[bool]" = True + type_coercion_capabilities: "ClassVar[TypeCoercionCapabilities]" = TypeCoercionCapabilities( + datetime_binding="native", timestamp_precision="microsecond", json_columns_decoded=False, uuid_binding="text" + ) + _connection_context_class: "ClassVar[type[SqlanywhereConnectionContext]]" = SqlanywhereConnectionContext + _session_factory_class: "ClassVar[type[_SqlanywhereSessionConnectionHandler]]" = ( + _SqlanywhereSessionConnectionHandler + ) + _session_context_class: "ClassVar[type[SqlanywhereSessionContext]]" = SqlanywhereSessionContext + _default_statement_config = default_statement_config + + def __init__( + self, + *, + connection_config: "SqlanywherePoolParams | dict[str, Any] | None" = None, + connection_instance: "SqlanywhereConnectionPool | None" = None, + migration_config: "dict[str, Any] | None" = None, + statement_config: "StatementConfig | None" = None, + driver_features: "SqlanywhereDriverFeatures | dict[str, Any] | None" = None, + bind_key: "str | None" = None, + extension_config: "ExtensionConfigs | None" = None, + observability_config: "ObservabilityConfig | None" = None, + **kwargs: Any, + ) -> None: + raw_config = dict(normalize_connection_config(connection_config)) + self._pool_min_size = int(raw_config.pop("pool_min_size", 1)) + self._pool_max_size = int(raw_config.pop("pool_max_size", 10)) + self._pool_timeout = float(raw_config.pop("pool_timeout", 30.0)) + self._pool_recycle_seconds = int(raw_config.pop("pool_recycle_seconds", 86400)) + self._health_check_interval = float(raw_config.pop("health_check_interval", 30.0)) + normalized_connection_config = build_connection_config(raw_config) + + statement_config = statement_config or default_statement_config + statement_config, driver_features = apply_driver_features(statement_config, driver_features) + + features_dict = dict(driver_features) if driver_features else {} + self._user_connection_hook: Callable[[SqlanywhereConnection], None] | None = features_dict.pop( + "on_connection_create", None + ) + + super().__init__( + connection_config=normalized_connection_config, + connection_instance=connection_instance, + migration_config=migration_config, + statement_config=statement_config, + driver_features=features_dict, + bind_key=bind_key, + extension_config=extension_config, + observability_config=observability_config, + **kwargs, + ) + + def create_connection(self) -> "SqlanywhereConnection": + """Open a standalone SAP SQL Anywhere connection owned by the caller.""" + return self.provide_pool().new_connection() + + def get_signature_namespace(self) -> "dict[str, Any]": + namespace = super().get_signature_namespace() + namespace.update({ + "SqlanywhereConfig": SqlanywhereConfig, + "SqlanywhereConnection": SqlanywhereConnection, + "SqlanywhereConnectionContext": SqlanywhereConnectionContext, + "SqlanywhereConnectionParams": SqlanywhereConnectionParams, + "SqlanywhereConnectionPool": SqlanywhereConnectionPool, + "SqlanywhereCursor": SqlanywhereCursor, + "SqlanywhereDriver": SqlanywhereDriver, + "SqlanywhereDriverFeatures": SqlanywhereDriverFeatures, + "SqlanywhereExceptionHandler": SqlanywhereExceptionHandler, + "SqlanywherePoolParams": SqlanywherePoolParams, + "SqlanywhereRawCursor": SqlanywhereRawCursor, + "SqlanywhereSessionContext": SqlanywhereSessionContext, + }) + return namespace + + def get_event_runtime_hints(self) -> "EventRuntimeHints": + return EventRuntimeHints(poll_interval=0.5, lease_seconds=30, select_for_update=False, skip_locked=False) + + def _create_pool(self) -> "SqlanywhereConnectionPool": + return SqlanywhereConnectionPool( + self.connection_config, + min_size=self._pool_min_size, + max_size=self._pool_max_size, + timeout=self._pool_timeout, + recycle_seconds=self._pool_recycle_seconds, + health_check_interval=self._health_check_interval, + on_connection_create=self._user_connection_hook, + ) + + def _close_pool(self) -> None: + if self.connection_instance: + self.connection_instance.close() + self.connection_instance = None diff --git a/sqlspec/adapters/sqlanywhere/core.py b/sqlspec/adapters/sqlanywhere/core.py new file mode 100644 index 000000000..7a8a690e1 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/core.py @@ -0,0 +1,403 @@ +"""SAP SQL Anywhere adapter compiled helpers.""" + +from collections.abc import Callable, Mapping, Sized +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Final, Literal + +from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile +from sqlspec.dialects.sybase import SQLAnywhere as _SQLAnywhereDialect +from sqlspec.exceptions import ( + CheckViolationError, + DatabaseConnectionError, + DataError, + DeadlockError, + ForeignKeyViolationError, + IntegrityError, + NotFoundError, + NotNullViolationError, + OperationalError, + PermissionDeniedError, + SerializationConflictError, + SQLParsingError, + SQLSpecError, + UniqueViolationError, +) +from sqlspec.utils.serializers import from_json, to_json +from sqlspec.utils.text import split_qualified_identifier +from sqlspec.utils.type_converters import build_uuid_coercions +from sqlspec.utils.type_guards import has_rowcount + +if TYPE_CHECKING: + from collections.abc import Sequence + from logging import Logger + +__all__ = ( + "DEFAULT_SQLANYWHERE_PORT", + "apply_driver_features", + "build_connection_config", + "build_insert_statement", + "build_profile", + "build_statement_config", + "collect_rows", + "create_mapped_exception", + "default_statement_config", + "driver_profile", + "escape_sql_literal", + "extract_sqlstate_and_sqlcode", + "format_identifier", + "normalize_execute_many_parameters", + "normalize_execute_parameters", + "quote_double_identifier", + "resolve_column_names", + "resolve_many_rowcount", + "resolve_rowcount", + "split_schema_and_table", +) + +DEFAULT_SQLANYWHERE_PORT: Final[int] = 2638 +_COLUMN_CACHE_MAX_SIZE: Final[int] = 256 +_SQLANYDB_FULL_ARGS_LEN: Final[int] = 3 +_SQLANYDB_PAIR_ARGS_LEN: Final[int] = 2 +_POOL_CONFIG_KEYS: Final[frozenset[str]] = frozenset({ + "pool_min_size", + "pool_max_size", + "pool_timeout", + "pool_recycle_seconds", + "health_check_interval", +}) +_SQLCODE_MAPPING: Final[dict[int, tuple[type[SQLSpecError], str]]] = { + -193: (UniqueViolationError, "primary key is not unique"), + -196: (UniqueViolationError, "unique index violation"), + -194: (ForeignKeyViolationError, "foreign key constraint violation"), + -198: (ForeignKeyViolationError, "primary key is referenced by foreign key"), + -195: (NotNullViolationError, "column cannot be NULL"), + -209: (CheckViolationError, "invalid value for column check constraint"), + -1091: (CheckViolationError, "check constraint violation"), + -306: (DeadlockError, "deadlock detected"), + -307: (DeadlockError, "all threads are blocked"), + -684: (DeadlockError, "rollback occurred due to deadlock"), + -210: (SerializationConflictError, "row is locked by another user"), + -1175: (SerializationConflictError, "snapshot isolation conflict"), + -131: (SQLParsingError, "SQL syntax error"), + -141: (NotFoundError, "table not found"), + -143: (NotFoundError, "column not found"), + -265: (NotFoundError, "procedure not found"), + -83: (DatabaseConnectionError, "specified database not found"), + -100: (DatabaseConnectionError, "database server not found"), + -103: (DatabaseConnectionError, "invalid user ID or password"), + -308: (DatabaseConnectionError, "connection was terminated"), + -832: (DatabaseConnectionError, "connection error"), + -101: (OperationalError, "not connected to a database"), + -157: (DataError, "data conversion error"), + -158: (DataError, "numeric value out of range"), + -638: (DataError, "right truncation of string data"), + -121: (PermissionDeniedError, "permission denied"), +} +_SQLSTATE_EXACT_MAPPING: Final[dict[str, tuple[type[SQLSpecError], str]]] = { + "23505": (UniqueViolationError, "unique constraint violation"), + "23503": (ForeignKeyViolationError, "foreign key constraint violation"), + "23502": (NotNullViolationError, "not-null constraint violation"), + "23506": (CheckViolationError, "check constraint violation"), + "23514": (CheckViolationError, "check constraint violation"), + "40001": (DeadlockError, "serialization/deadlock failure"), + "42000": (SQLParsingError, "SQL syntax error"), + "42S02": (NotFoundError, "table not found"), + "42S22": (NotFoundError, "column not found"), + "42501": (PermissionDeniedError, "permission denied"), + "28000": (DatabaseConnectionError, "invalid authorization specification"), +} + + +def quote_double_identifier(identifier: str) -> str: + """Quote a SQL Anywhere identifier with double quotes.""" + cleaned = identifier.strip() + if (cleaned.startswith('"') and cleaned.endswith('"')) or (cleaned.startswith("[") and cleaned.endswith("]")): + cleaned = cleaned[1:-1].replace('""', '"').replace("]]", "]") + return f'"{cleaned.replace(chr(34), chr(34) * 2)}"' + + +def escape_sql_literal(value: str) -> str: + """Escape single quotes inside a SQL string literal.""" + return value.replace("'", "''") + + +def split_schema_and_table(identifier: str, *, default_schema: str = "DBA") -> tuple[str, str]: + """Split a table reference into unquoted ``(schema, table)`` names.""" + cleaned = identifier.strip() + if not cleaned: + msg = "Table name must not be empty" + raise SQLSpecError(msg) + parts = split_qualified_identifier(cleaned, quote_chars='"', allow_bracket_quotes=True) + if not parts: + return default_schema, cleaned + if len(parts) == 1: + return default_schema, parts[0] + return parts[-2], parts[-1] + + +def format_identifier(identifier: str) -> str: + """Format a SQL Anywhere identifier with double-quote quoting.""" + cleaned = identifier.strip() + if not cleaned: + msg = "Table name must not be empty" + raise SQLSpecError(msg) + parts = split_qualified_identifier(cleaned, quote_chars='"', allow_bracket_quotes=True) + return ".".join(quote_double_identifier(part) for part in parts) + + +def build_insert_statement(table: str, columns: "list[str]") -> str: + """Build a QMARK-parameterized INSERT statement for SQL Anywhere.""" + column_clause = ", ".join(quote_double_identifier(column) for column in columns) + placeholders = ", ".join("?" for _ in columns) + return f"INSERT INTO {format_identifier(table)} ({column_clause}) VALUES ({placeholders})" + + +def normalize_execute_parameters(parameters: Any) -> Any: + """Normalize parameters for SQL Anywhere sqlanydb execute calls.""" + if parameters is None: + return () + if isinstance(parameters, list): + return tuple(parameters) + return parameters + + +def normalize_execute_many_parameters(parameters: Any) -> Any: + """Normalize parameters for SQL Anywhere sqlanydb executemany calls.""" + return parameters + + +def build_profile() -> "DriverParameterProfile": + """Create the SAP SQL Anywhere driver parameter profile.""" + return DriverParameterProfile( + name="sqlanywhere", + default_style=ParameterStyle.QMARK, + supported_styles={ + ParameterStyle.QMARK, + ParameterStyle.NUMERIC, + ParameterStyle.NAMED_COLON, + ParameterStyle.NAMED_PYFORMAT, + ParameterStyle.POSITIONAL_PYFORMAT, + }, + default_execution_style=ParameterStyle.QMARK, + supported_execution_styles={ParameterStyle.QMARK}, + has_native_list_expansion=False, + preserve_parameter_format=True, + needs_static_script_compilation=False, + allow_mixed_parameter_styles=False, + preserve_original_params_for_many=False, + json_serializer_strategy="helper", + custom_type_coercions=_custom_type_coercions(), + default_dialect="sqlanywhere", + ) + + +def build_statement_config( + *, json_serializer: "Callable[[Any], str] | None" = None, json_deserializer: "Callable[[str], Any] | None" = None +) -> "StatementConfig": + """Construct the SAP SQL Anywhere statement configuration.""" + return build_statement_config_from_profile( + driver_profile, + statement_overrides={"dialect": "sqlanywhere"}, + json_serializer=json_serializer or to_json, + json_deserializer=json_deserializer or from_json, + ) + + +def apply_driver_features( + statement_config: "StatementConfig", driver_features: "Mapping[str, Any] | None" +) -> "tuple[StatementConfig, dict[str, Any]]": + """Apply SQL Anywhere driver feature overrides to statement config.""" + features: dict[str, Any] = dict(driver_features) if driver_features else {} + json_serializer = features.setdefault("json_serializer", to_json) + json_deserializer = features.setdefault("json_deserializer", from_json) + + if json_serializer is not None: + parameter_config = statement_config.parameter_config.with_json_serializers( + json_serializer, deserializer=json_deserializer + ) + statement_config = statement_config.replace(parameter_config=parameter_config) + + return statement_config, features + + +def build_connection_config(connection_config: "Mapping[str, Any]") -> "dict[str, Any]": + """Normalize connection config into ``sqlanydb.connect(**kwargs)`` parameters. + + Maps friendly SQLSpec keys into SAP SQL Anywhere native connection string keys: + - ``user`` / ``username`` -> ``uid`` + - ``password`` -> ``pwd`` + - ``database`` / ``db`` -> ``dbn`` + - ``server`` / ``engine`` / ``server_name`` -> ``eng`` + - ``database_file`` -> ``dbf`` + - ``host`` + ``port`` -> ``host="host:port"`` (default port ``2638``) + """ + normalized: dict[str, Any] = dict(connection_config) + + extra = normalized.pop("extra", None) + if isinstance(extra, Mapping): + for key, value in extra.items(): + if value is not None: + normalized.setdefault(key, value) + + for pool_key in _POOL_CONFIG_KEYS: + normalized.pop(pool_key, None) + + for alias_key in ("user", "username"): + alias_val = normalized.pop(alias_key, None) + if alias_val is not None and "uid" not in normalized: + normalized["uid"] = alias_val + + password_val = normalized.pop("password", None) + if password_val is not None and "pwd" not in normalized: + normalized["pwd"] = password_val + + for db_key in ("database", "db"): + db_val = normalized.pop(db_key, None) + if db_val is not None and "dbn" not in normalized: + normalized["dbn"] = db_val + + for eng_key in ("server", "engine", "server_name"): + eng_val = normalized.pop(eng_key, None) + if eng_val is not None and "eng" not in normalized: + normalized["eng"] = eng_val + + dbf_val = normalized.pop("database_file", None) + if dbf_val is not None and "dbf" not in normalized: + normalized["dbf"] = dbf_val + + port_val = normalized.pop("port", None) + host_val = normalized.get("host") + if isinstance(host_val, str) and host_val and ":" not in host_val: + resolved_port = int(port_val) if port_val is not None else DEFAULT_SQLANYWHERE_PORT + normalized["host"] = f"{host_val}:{resolved_port}" + + return {key: value for key, value in normalized.items() if value is not None} + + +def resolve_column_names( + description: "Sequence[Any] | None", cache: "dict[int, tuple[Sequence[Any], list[str]]]" +) -> "list[str]": + """Resolve and cache column names from cursor description.""" + if not description: + return [] + desc_id = id(description) + cached = cache.get(desc_id) + if cached is not None and cached[0] is description: + return cached[1] + names = [col[0] for col in description] + if len(cache) >= _COLUMN_CACHE_MAX_SIZE: + cache.pop(next(iter(cache))) + cache[desc_id] = (description, names) + return names + + +def collect_rows( + fetched_data: "Sequence[Any] | None", + description: "Sequence[Any] | None", + cache: "dict[int, tuple[Sequence[Any], list[str]]]", +) -> "tuple[list[Any], list[str], Literal['dict', 'tuple']]": + """Collect SQL Anywhere fetched rows into a row list, column names, and row format.""" + if not description: + return [], [], "tuple" + column_names = resolve_column_names(description, cache) + if not fetched_data: + return [], column_names, "tuple" + data = fetched_data if isinstance(fetched_data, list) else list(fetched_data) + row_format: Literal["dict", "tuple"] = "dict" if isinstance(data[0], dict) else "tuple" + return data, column_names, row_format + + +def resolve_rowcount(cursor: Any) -> int: + """Resolve rowcount from a sqlanydb cursor.""" + if not has_rowcount(cursor): + return 0 + rowcount = cursor.rowcount + if isinstance(rowcount, int): + return max(rowcount, 0) + return 0 + + +def resolve_many_rowcount(cursor: Any, parameters: Any, *, fallback_count: int | None = None) -> int: + """Resolve affected row count for SQL Anywhere executemany.""" + rowcount = resolve_rowcount(cursor) + if rowcount > 0: + return rowcount + if fallback_count is not None: + return fallback_count + if isinstance(parameters, Sized): + return len(parameters) + return 0 + + +def extract_sqlstate_and_sqlcode(error: BaseException) -> tuple[str | None, int | None]: + """Extract ``(sqlstate, sqlcode)`` from a ``sqlanydb.Error`` instance.""" + sqlstate: str | None = getattr(error, "sqlstate", None) + sqlcode: int | None = getattr(error, "errorcode", getattr(error, "sqlcode", None)) + + args: tuple[Any, ...] = error.args if isinstance(getattr(error, "args", None), tuple) else () + if len(args) >= _SQLANYDB_FULL_ARGS_LEN: + if sqlstate is None and isinstance(args[0], str): + sqlstate = args[0] + if sqlcode is None and isinstance(args[1], int): + sqlcode = args[1] + elif len(args) == _SQLANYDB_PAIR_ARGS_LEN: + if isinstance(args[0], int) and sqlcode is None: + sqlcode = args[0] + elif isinstance(args[0], str) and sqlstate is None: + sqlstate = args[0] + if isinstance(args[1], int) and sqlcode is None: + sqlcode = args[1] + elif len(args) == 1 and isinstance(args[0], int) and sqlcode is None: + sqlcode = args[0] + + return sqlstate, sqlcode + + +def create_mapped_exception(error: BaseException, *, logger: "Logger | None" = None) -> SQLSpecError: + """Map a sqlanydb exception into the SQLSpec exception hierarchy.""" + _ = logger + sqlstate, sqlcode = extract_sqlstate_and_sqlcode(error) + + if sqlcode is not None and sqlcode in _SQLCODE_MAPPING: + exc_cls, label = _SQLCODE_MAPPING[sqlcode] + return _create_error(error, sqlcode, sqlstate, exc_cls, label) + + if sqlstate is not None: + upper_state = sqlstate.upper() + if upper_state in _SQLSTATE_EXACT_MAPPING: + exc_cls, label = _SQLSTATE_EXACT_MAPPING[upper_state] + return _create_error(error, sqlcode, sqlstate, exc_cls, label) + if upper_state.startswith("08"): + return _create_error(error, sqlcode, sqlstate, DatabaseConnectionError, "connection error") + if upper_state.startswith("22"): + return _create_error(error, sqlcode, sqlstate, DataError, "data error") + if upper_state.startswith("23"): + return _create_error(error, sqlcode, sqlstate, IntegrityError, "integrity constraint violation") + + return _create_error(error, sqlcode, sqlstate, SQLSpecError, "database error") + + +def _bool_to_int(value: bool) -> int: + return int(value) + + +def _custom_type_coercions() -> "dict[type, Callable[[Any], Any]]": + return {bool: _bool_to_int, Decimal: str, **build_uuid_coercions()} + + +def _create_error( + error: BaseException, sqlcode: int | None, sqlstate: str | None, error_class: type[SQLSpecError], description: str +) -> SQLSpecError: + code_part = f"sqlcode={sqlcode}" if sqlcode is not None else "" + state_part = f"sqlstate={sqlstate}" if sqlstate is not None else "" + qualifier = ", ".join(part for part in (code_part, state_part) if part) + prefix = f"SQL Anywhere {description} [{qualifier}]" if qualifier else f"SQL Anywhere {description}" + msg = f"{prefix}: {error}" + exc = error_class(msg) + exc.__cause__ = error + return exc + + +_ = _SQLAnywhereDialect +driver_profile = build_profile() +default_statement_config = build_statement_config() diff --git a/sqlspec/adapters/sqlanywhere/data_dictionary.py b/sqlspec/adapters/sqlanywhere/data_dictionary.py new file mode 100644 index 000000000..f9fe77395 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/data_dictionary.py @@ -0,0 +1,279 @@ +"""SAP SQL Anywhere database data dictionary implementation.""" + +import logging +from typing import TYPE_CHECKING, Any, ClassVar, cast + +from mypy_extensions import mypyc_attr + +from sqlspec.data_dictionary import ( + ColumnMetadata, + ForeignKeyMetadata, + IndexMetadata, + MetadataCapabilityProfile, + MetadataResult, + TableMetadata, +) +from sqlspec.data_dictionary.dialects.sqlanywhere import ( + SQLANYWHERE_CONFIG, + SQLANYWHERE_VERSION_PATTERN, + SqlanywhereVersionInfo, + build_sqlanywhere_metadata_capability_profile, + extract_sqlanywhere_version_value, + list_sqlanywhere_available_features, + merge_sqlanywhere_table_lists, + resolve_sqlanywhere_feature_flag, +) +from sqlspec.driver import SyncDataDictionaryBase +from sqlspec.utils.logging import get_logger, log_with_context + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from sqlspec.adapters.sqlanywhere.driver import SqlanywhereDriver + +__all__ = ( + "SQLANYWHERE_CONFIG", + "SqlanywhereDataDictionary", + "SqlanywhereSyncDataDictionary", + "SqlanywhereVersionInfo", + "build_column_metadata", + "build_foreign_key_metadata", + "build_index_metadata", + "parse_sqlanywhere_version", +) + +logger = get_logger("sqlspec.adapters.sqlanywhere.data_dictionary") +_VERSION_PARTS_WITH_BUILD = 4 + + +def parse_sqlanywhere_version(row: "dict[str, Any] | None") -> "SqlanywhereVersionInfo | None": + """Parse the SQL Anywhere version string returned by the version query.""" + version_str = extract_sqlanywhere_version_value(row) + match = SQLANYWHERE_VERSION_PATTERN.search(version_str) if version_str else None + if match is None: + log_with_context( + logger, logging.DEBUG, "data_dictionary.version.unparsed", dialect="sqlanywhere", value=version_str + ) + return None + patch_val = int(match.group(3)) if match.group(3) else 0 + parts = version_str.split(".") if version_str else [] + build_number = parts[3].split()[0] if len(parts) >= _VERSION_PARTS_WITH_BUILD else None + return SqlanywhereVersionInfo(int(match.group(1)), int(match.group(2)), patch_val, build_number=build_number) + + +def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetadata]": + """Convert SQL Anywhere SYS.SYSTABCOL query rows to column metadata.""" + return [ + ColumnMetadata( + schema_name=str(r.get("schema_name", "")), + table_name=str(r.get("table_name", "")), + column_name=str(r.get("column_name", "")), + data_type=str(r.get("data_type", "")), + is_nullable=bool(r.get("is_nullable", True)), + column_default=str(r["column_default"]) if r.get("column_default") is not None else None, + ordinal_position=int(r.get("ordinal_position", 1)), + max_length=int(r.get("max_length", 0)) if r.get("max_length") is not None else 0, + numeric_scale=int(r.get("numeric_scale", 0)) if r.get("numeric_scale") is not None else 0, + is_primary=bool(r.get("is_primary")), + is_unique=bool(r.get("is_unique")), + identity_generation=str(r["identity_generation"]) + if r.get("identity_generation") + else ("BY DEFAULT" if r.get("is_identity") else None), + is_generated=bool(r.get("is_generated")), + ) + for r in rows + ] + + +def build_index_metadata(rows: "Iterable[dict[str, Any]]") -> "list[IndexMetadata]": + """Group per-column SQL Anywhere SYS.SYSIDX rows into index metadata.""" + indexes_map: dict[tuple[str, str, str], dict[str, Any]] = {} + for r in rows: + key = (str(r.get("schema_name", "")), str(r.get("table_name", "")), str(r.get("index_name", ""))) + if key not in indexes_map: + indexes_map[key] = { + "schema_name": key[0], + "table_name": key[1], + "index_name": key[2], + "columns": [], + "is_unique": bool(r.get("is_unique", 0)), + "is_primary": bool(r.get("is_primary", 0)), + } + col_name = str(r.get("column_name", "")) + if col_name: + indexes_map[key]["columns"].append(col_name) + + return [ + IndexMetadata( + schema_name=idx["schema_name"], + table_name=idx["table_name"], + index_name=idx["index_name"], + columns=idx["columns"], + is_unique=idx["is_unique"], + is_primary=idx["is_primary"], + ) + for idx in indexes_map.values() + ] + + +def build_foreign_key_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ForeignKeyMetadata]": + """Convert SQL Anywhere SYS.SYSFOREIGNKEY query rows to foreign key metadata.""" + return [ + ForeignKeyMetadata( + table_name=str(r.get("table_name", "")), + column_name=str(r.get("column_name", "")), + referenced_table=str(r.get("referenced_table_name", "")), + referenced_column=str(r.get("referenced_column_name", "")), + constraint_name=str(r.get("constraint_name", "")), + schema=str(r["schema_name"]) if r.get("schema_name") else None, + referenced_schema=str(r["referenced_schema"]) if r.get("referenced_schema") else None, + ) + for r in rows + ] + + +@mypyc_attr(allow_interpreted_subclasses=True, native_class=False) +class SqlanywhereSyncDataDictionary(SyncDataDictionaryBase): + """Synchronous SAP SQL Anywhere data dictionary.""" + + dialect: ClassVar[str] = "sqlanywhere" + + def __init__(self) -> None: + super().__init__() + + def get_metadata_capabilities( + self, driver: "SqlanywhereDriver", domains: "Sequence[str] | None" = None + ) -> MetadataCapabilityProfile: + """Return the SQL Anywhere metadata capability profile.""" + _ = driver + return build_sqlanywhere_metadata_capability_profile(type(self).__name__, domains) + + def get_version(self, driver: "SqlanywhereDriver") -> "SqlanywhereVersionInfo | None": + """Detect and cache the SQL Anywhere server version.""" + driver_id = id(driver) + if driver_id in self._version_fetch_attempted: + return cast("SqlanywhereVersionInfo | None", self._version_cache.get(driver_id)) + + version_row = driver.select_one_or_none(self.get_query("version", "current")) + version_info = parse_sqlanywhere_version(version_row) + if version_info is None: + self._log_version_unavailable(type(self).dialect, "missing") + else: + self._log_version_detected(type(self).dialect, version_info) + self.cache_version(driver_id, version_info) + return version_info + + def get_feature_flag(self, driver: "SqlanywhereDriver", feature: str) -> bool: + """Check whether the connected SQL Anywhere server supports a feature.""" + version_info = self.get_version(driver) + return resolve_sqlanywhere_feature_flag(feature, version_info) + + def get_optimal_type(self, driver: "SqlanywhereDriver", type_category: str) -> str: + """Return the optimal SQL Anywhere SQL type for a logical category.""" + _ = driver + return SQLANYWHERE_CONFIG.get_optimal_type(type_category) + + def list_available_features(self) -> "list[str]": + """List feature flags defined for the SQL Anywhere dialect.""" + return list_sqlanywhere_available_features() + + def get_tables(self, driver: "SqlanywhereDriver", schema: "str | None" = None) -> "list[TableMetadata]": + """Return tables in topological dependency order for a SQL Anywhere schema.""" + schema_name = self.resolve_schema(schema) + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="tables") + ordered_rows = cast( + "list[TableMetadata]", + driver.select( + self.get_query("tables", "by_schema"), + schema_name=schema_name, + table_name=None, + schema_type=TableMetadata, + ), + ) + all_rows = cast( + "list[TableMetadata]", + driver.select( + self.get_query("tables", "all_by_schema"), + schema_name=schema_name, + table_name=None, + schema_type=TableMetadata, + ), + ) + return merge_sqlanywhere_table_lists(ordered_rows, all_rows) + + def get_columns( + self, driver: "SqlanywhereDriver", table: "str | None" = None, schema: "str | None" = None + ) -> "list[ColumnMetadata]": + """Return column metadata for a table or entire SQL Anywhere schema.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="columns") + rows = driver.select(self.get_query("columns", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="columns") + rows = driver.select(self.get_query("columns", "by_table"), schema_name=schema_name, table_name=table_name) + return build_column_metadata(rows) + + def get_indexes( + self, driver: "SqlanywhereDriver", table: "str | None" = None, schema: "str | None" = None + ) -> "list[IndexMetadata]": + """Return index metadata for a table or entire SQL Anywhere schema.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="indexes") + rows = driver.select(self.get_query("indexes", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="indexes") + rows = driver.select(self.get_query("indexes", "by_table"), schema_name=schema_name, table_name=table_name) + return build_index_metadata(rows) + + def get_foreign_keys( + self, driver: "SqlanywhereDriver", table: "str | None" = None, schema: "str | None" = None + ) -> "list[ForeignKeyMetadata]": + """Return foreign key metadata for a table or entire SQL Anywhere schema.""" + schema_name = self.resolve_schema(schema) + if table is None: + self._log_schema_introspect(driver, schema_name=schema_name, table_name=None, operation="foreign_keys") + rows = driver.select(self.get_query("foreign_keys", "by_schema"), schema_name=schema_name, table_name=None) + else: + table_name = self.resolve_identifier(table) + self._log_table_describe(driver, schema_name=schema_name, table_name=table_name, operation="foreign_keys") + rows = driver.select( + self.get_query("foreign_keys", "by_table"), schema_name=schema_name, table_name=table_name + ) + return build_foreign_key_metadata(rows) + + def get_constraints( + self, driver: "SqlanywhereDriver", table: "str | None" = None, schema: "str | None" = None + ) -> MetadataResult: + """Get SQL Anywhere constraint metadata.""" + schema_name = self.resolve_schema(schema) + table_name = self.resolve_identifier(table) if table is not None else None + query = self.get_query("constraints", "by_table" if table_name else "by_schema") + rows = driver.select(query, schema_name=schema_name, table_name=table_name) + return MetadataResult("constraints", items=tuple(rows)) + + def get_views(self, driver: "SqlanywhereDriver", schema: "str | None" = None) -> MetadataResult: + """Get SQL Anywhere view metadata.""" + schema_name = self.resolve_schema(schema) + query = self.get_query("views", "by_schema") + rows = driver.select(query, schema_name=schema_name, view_name=None) + return MetadataResult("views", items=tuple(rows)) + + def get_objects(self, driver: "SqlanywhereDriver", schema: "str | None" = None) -> MetadataResult: + """Get tables, views, and procedures from the SQL Anywhere catalog.""" + rows = driver.select( + self.get_query("objects", "by_schema"), schema_name=self.resolve_schema(schema), object_name=None + ) + return MetadataResult("objects", items=tuple(rows)) + + def get_schemas(self, driver: "SqlanywhereDriver") -> MetadataResult: + """Get SQL Anywhere schema metadata.""" + query = self.get_query("schemas", "by_schema") + rows = driver.select(query, schema_name=None) + return MetadataResult("schemas", items=tuple(rows)) + + +SqlanywhereDataDictionary = SqlanywhereSyncDataDictionary diff --git a/sqlspec/adapters/sqlanywhere/driver.py b/sqlspec/adapters/sqlanywhere/driver.py new file mode 100644 index 000000000..0d3cc60ac --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/driver.py @@ -0,0 +1,354 @@ +"""SAP SQL Anywhere database driver implementation over sqlanydb.""" + +import contextlib +from collections.abc import Sized +from typing import TYPE_CHECKING, Any, cast + +from sqlspec.adapters.sqlanywhere._typing import ( + SqlanywhereConnection, + SqlanywhereCursor, + SqlanywhereError, + SqlanywhereRawCursor, + SqlanywhereSessionContext, +) +from sqlspec.adapters.sqlanywhere.core import ( + build_insert_statement, + collect_rows, + create_mapped_exception, + default_statement_config, + driver_profile, + format_identifier, + normalize_execute_many_parameters, + normalize_execute_parameters, + resolve_column_names, + resolve_many_rowcount, + resolve_rowcount, +) +from sqlspec.adapters.sqlanywhere.data_dictionary import SqlanywhereSyncDataDictionary +from sqlspec.core import SQL, StatementConfig, get_cache_config, register_driver_profile +from sqlspec.driver import ( + BaseSyncExceptionHandler, + ExecutionResult, + SyncDriverAdapterBase, + SyncRowStream, + rows_to_dicts, + validate_savepoint_name, +) +from sqlspec.exceptions import SQLSpecError +from sqlspec.utils.logging import get_logger + +if TYPE_CHECKING: + from collections.abc import Sequence + + from sqlspec.adapters.sqlanywhere._typing import SqlanywhereQueryParams as QueryParams + from sqlspec.builder import QueryBuilder + from sqlspec.core import ArrowResult, SQLResult, Statement, StatementFilter + from sqlspec.driver import CachedQuery + from sqlspec.storage import StorageBridgeJob, StorageDestination, StorageFormat, StorageTelemetry + from sqlspec.typing import StatementParameters + +__all__ = ( + "SqlanywhereCursor", + "SqlanywhereDriver", + "SqlanywhereExceptionHandler", + "SqlanywhereSessionContext", + "SqlanywhereStreamSource", +) + +logger = get_logger("sqlspec.adapters.sqlanywhere") + + +class SqlanywhereExceptionHandler(BaseSyncExceptionHandler): + """Context manager for handling SAP SQL Anywhere sqlanydb exceptions.""" + + __slots__ = () + + def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: + if exc_type is None: + return False + if isinstance(exc_val, (SqlanywhereError, Exception)) and not isinstance(exc_val, SQLSpecError): + self.pending_exception = create_mapped_exception(exc_val, logger=logger) + return True + return False + + +class SqlanywhereStreamSource: + """Native SQL Anywhere sqlanydb chunk source backed by ``cursor.fetchmany()``.""" + + __slots__ = ("_chunk_size", "_column_names", "_cursor_manager", "_driver", "_parameters", "_sql") + + def __init__(self, driver: "SqlanywhereDriver", sql: str, parameters: Any, chunk_size: int) -> None: + self._driver = driver + self._sql = sql + self._parameters = parameters + self._chunk_size = chunk_size + self._cursor_manager: SqlanywhereCursor | None = None + self._column_names: list[str] | None = None + + def start(self) -> None: + cursor_manager = self._driver.with_cursor(self._driver.connection) + try: + cursor = cursor_manager.__enter__() + handler = self._driver.handle_database_exceptions() + with handler: + normalized_params = normalize_execute_parameters(self._parameters) + if normalized_params: + cursor.execute(self._sql, normalized_params) + else: + cursor.execute(self._sql) + self._driver._check_pending_exception(handler) + except BaseException: + with contextlib.suppress(Exception): + cursor_manager.__exit__(None, None, None) + raise + self._cursor_manager = cursor_manager + + def fetch_chunk(self) -> "list[dict[str, Any]]": + cursor_manager = self._cursor_manager + if cursor_manager is None or cursor_manager.cursor is None: + return [] + cursor = cursor_manager.cursor + handler = self._driver.handle_database_exceptions() + rows: Any = [] + with handler: + rows = cursor.fetchmany(self._chunk_size) + self._driver._check_pending_exception(handler) + if not rows: + return [] + column_names = self._column_names + if column_names is None: + column_names = resolve_column_names(cursor.description or None, self._driver._column_name_cache) + self._column_names = column_names + return rows_to_dicts(rows, column_names) + + def close(self, error: bool = False) -> None: + _ = error + cursor_manager = self._cursor_manager + self._cursor_manager = None + if cursor_manager is not None: + with contextlib.suppress(Exception): + cursor_manager.__exit__(None, None, None) + + +class SqlanywhereDriver(SyncDriverAdapterBase): + """SAP SQL Anywhere database driver using sqlanydb.""" + + __slots__ = ("_column_name_cache", "_data_dictionary", "_transaction_active") + dialect = "sqlanywhere" + + def __init__( + self, + connection: "SqlanywhereConnection", + statement_config: "StatementConfig | None" = None, + driver_features: "dict[str, Any] | None" = None, + ) -> None: + if statement_config is None: + statement_config = default_statement_config.replace( + enable_caching=get_cache_config().compiled_cache_enabled + ) + + super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features) + self._data_dictionary: SqlanywhereSyncDataDictionary | None = None + self._column_name_cache: dict[int, tuple[Any, list[str]]] = {} + self._transaction_active = False + + @property + def data_dictionary(self) -> "SqlanywhereSyncDataDictionary": + if self._data_dictionary is None: + self._data_dictionary = SqlanywhereSyncDataDictionary() + return self._data_dictionary + + def dispatch_execute(self, cursor: "SqlanywhereRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + normalized_params = normalize_execute_parameters(prepared_parameters) + if normalized_params: + cursor.execute(sql, normalized_params) + else: + cursor.execute(sql) + + if statement.returns_rows(): + fetched_data = cursor.fetchall() + description = cursor.description or None + rows, column_names, row_format = collect_rows(fetched_data, description, self._column_name_cache) + return self.create_execution_result( + cursor, + selected_data=rows, + column_names=column_names, + data_row_count=len(rows), + is_select_result=True, + row_format=row_format, + ) + + return self.create_execution_result(cursor, rowcount_override=resolve_rowcount(cursor)) + + def dispatch_execute_many(self, cursor: "SqlanywhereRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + prepared_parameters = normalize_execute_many_parameters(prepared_parameters) + parameter_count = len(prepared_parameters) if isinstance(prepared_parameters, Sized) else None + cursor.executemany(sql, cast("Sequence[QueryParams]", prepared_parameters)) + + affected_rows = resolve_many_rowcount(cursor, prepared_parameters, fallback_count=parameter_count) + return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True) + + def dispatch_execute_script(self, cursor: "SqlanywhereRawCursor", statement: "SQL") -> "ExecutionResult": + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + statements = self.split_script_statements(sql, statement.statement_config, strip_trailing_semicolon=True) + normalized_params = normalize_execute_parameters(prepared_parameters) + + successful_count = 0 + for stmt in statements: + if normalized_params: + cursor.execute(stmt, normalized_params) + else: + cursor.execute(stmt) + successful_count += 1 + return self.create_execution_result( + cursor, statement_count=len(statements), successful_statements=successful_count, is_script_result=True + ) + + def begin(self) -> None: + """Mark a transaction as active on the SQL Anywhere DB-API 2.0 connection.""" + self._transaction_active = True + + def commit(self) -> None: + try: + self.connection.commit() + self._transaction_active = False + except Exception as exc: + msg = f"Failed to commit SQL Anywhere transaction: {exc}" + raise SQLSpecError(msg) from exc + + def rollback(self) -> None: + try: + self.connection.rollback() + except Exception as exc: + msg = f"Failed to rollback SQL Anywhere transaction: {exc}" + raise SQLSpecError(msg) from exc + finally: + self._transaction_active = False + + def with_cursor(self, connection: "SqlanywhereConnection") -> "SqlanywhereCursor": + return SqlanywhereCursor(connection) + + def handle_database_exceptions(self) -> "SqlanywhereExceptionHandler": + return SqlanywhereExceptionHandler() + + def dispatch_select_stream(self, statement: "SQL", chunk_size: int) -> "SyncRowStream[dict[str, Any]] | None": + """Return a native SQL Anywhere sqlanydb row stream backed by ``fetchmany()``.""" + if not statement.returns_rows(): + return None + sql, prepared_parameters = self._compiled_sql(statement, self.statement_config) + return SyncRowStream(SqlanywhereStreamSource(self, sql, prepared_parameters, chunk_size)) + + def create_savepoint(self, name: str) -> None: + self.execute_script(f"SAVEPOINT {validate_savepoint_name(name)}") + + def release_savepoint(self, name: str) -> None: + self.execute_script(f"RELEASE SAVEPOINT {validate_savepoint_name(name)}") + + def rollback_to_savepoint(self, name: str) -> None: + self.execute_script(f"ROLLBACK TO SAVEPOINT {validate_savepoint_name(name)}") + + def has_schema(self, schema: str) -> bool: + """Return whether the specified user/schema exists in SYS.SYSUSER.""" + result = False + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.execute("SELECT 1 FROM SYS.SYSUSER WHERE user_name = ?", (schema,)) + result = cursor.fetchone() is not None + self._check_pending_exception(exc_handler) + return result + + def select_to_storage( + self, + statement: "Statement | QueryBuilder | SQL | str", + destination: "StorageDestination", + /, + *parameters: "StatementParameters | StatementFilter", + statement_config: "StatementConfig | None" = None, + partitioner: "dict[str, object] | None" = None, + format_hint: "StorageFormat | None" = None, + telemetry: "StorageTelemetry | None" = None, + **kwargs: Any, + ) -> "StorageBridgeJob": + """Execute a query and write the Arrow result to storage.""" + self._require_capability("arrow_export_enabled") + arrow_result = self.select_to_arrow(statement, *parameters, statement_config=statement_config, **kwargs) + pipeline = self._storage_pipeline() + telemetry_payload = self._write_storage_result( + arrow_result, destination, format_hint=format_hint, pipeline=pipeline + ) + self._attach_partition_telemetry(telemetry_payload, partitioner) + return self._storage_job(telemetry_payload, telemetry) + + def load_from_arrow( + self, + table: str, + source: "ArrowResult | Any", + *, + partitioner: "dict[str, object] | None" = None, + overwrite: bool = False, + telemetry: "StorageTelemetry | None" = None, + ) -> "StorageBridgeJob": + """Load Arrow data into the target SQL Anywhere table via executemany.""" + self._require_capability("arrow_import_enabled") + arrow_table = self._coerce_arrow_table(source) + if overwrite: + statement = f"DELETE FROM {format_identifier(table)}" + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.execute(statement) + self._check_pending_exception(exc_handler) + + columns, records = self._arrow_table_to_rows(arrow_table) + if records: + insert_sql = build_insert_statement(table, columns) + needs_preparation = self._arrow_rows_need_preparation(arrow_table) + prepared_records = ( + self.prepare_driver_parameters(records, self.statement_config, is_many=True) + if needs_preparation + else records + ) + normalized_records = normalize_execute_many_parameters(prepared_records) + exc_handler = self.handle_database_exceptions() + with exc_handler, self.with_cursor(self.connection) as cursor: + cursor.executemany(insert_sql, cast("Sequence[QueryParams]", normalized_records)) + self._check_pending_exception(exc_handler) + + telemetry_payload = self._ingest_telemetry(arrow_table) + telemetry_payload["destination"] = table + self._attach_partition_telemetry(telemetry_payload, partitioner) + return self._storage_job(telemetry_payload, telemetry) + + def load_from_storage( + self, + table: str, + source: "StorageDestination", + *, + file_format: "StorageFormat", + partitioner: "dict[str, object] | None" = None, + overwrite: bool = False, + ) -> "StorageBridgeJob": + """Load staged artifacts from storage into the target SQL Anywhere table.""" + arrow_table, inbound = self._read_storage_arrow(source, file_format=file_format) + return self.load_from_arrow(table, arrow_table, partitioner=partitioner, overwrite=overwrite, telemetry=inbound) + + def collect_rows(self, cursor: "SqlanywhereRawCursor", fetched: "list[Any]") -> "tuple[list[Any], list[str], int]": + rows, column_names, _row_format = collect_rows(fetched, cursor.description or None, self._column_name_cache) + return rows, column_names, len(rows) + + def resolve_rowcount(self, cursor: "SqlanywhereRawCursor") -> int: + return resolve_rowcount(cursor) + + def _execute_cache_hit( + self, sql: str, params: "tuple[Any, ...] | list[Any] | dict[str, Any]", cached: "CachedQuery" + ) -> "SQLResult": + normalized_params = normalize_execute_parameters(params) if isinstance(params, list) else params + return super()._execute_cache_hit(sql, normalized_params, cached) + + def _connection_in_transaction(self) -> bool: + """Return whether a transaction opened by this driver remains active.""" + return self._transaction_active + + +register_driver_profile("sqlanywhere", driver_profile) diff --git a/sqlspec/adapters/sqlanywhere/events/__init__.py b/sqlspec/adapters/sqlanywhere/events/__init__.py new file mode 100644 index 000000000..70252c8ad --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/events/__init__.py @@ -0,0 +1,5 @@ +"""Events integration for SAP SQL Anywhere adapter.""" + +from sqlspec.adapters.sqlanywhere.events.store import SqlanywhereEventQueueStore, SqlanywhereSyncEventQueueStore + +__all__ = ("SqlanywhereEventQueueStore", "SqlanywhereSyncEventQueueStore") diff --git a/sqlspec/adapters/sqlanywhere/events/store.py b/sqlspec/adapters/sqlanywhere/events/store.py new file mode 100644 index 000000000..9a4e23b35 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/events/store.py @@ -0,0 +1,31 @@ +"""SAP SQL Anywhere event queue store with SQL Anywhere-compatible DDL.""" + +from sqlspec.adapters.sqlanywhere.config import SqlanywhereConfig +from sqlspec.extensions.events import BaseEventQueueStore + +__all__ = ("SqlanywhereEventQueueStore", "SqlanywhereSyncEventQueueStore") + +_VARCHAR_TEXT_THRESHOLD = 4000 + + +class SqlanywhereSyncEventQueueStore(BaseEventQueueStore[SqlanywhereConfig]): + """SAP SQL Anywhere DDL hooks for the event queue store.""" + + __slots__ = () + + def _column_types(self) -> tuple[str, str, str]: + return "LONG VARCHAR", "LONG VARCHAR", "TIMESTAMP" + + def _string_type(self, length: int) -> str: + if length >= _VARCHAR_TEXT_THRESHOLD: + return "LONG VARCHAR" + return f"VARCHAR({length})" + + def _integer_type(self) -> str: + return "INTEGER" + + def _timestamp_default(self) -> str: + return "CURRENT UTC TIMESTAMP" + + +SqlanywhereEventQueueStore = SqlanywhereSyncEventQueueStore diff --git a/sqlspec/adapters/sqlanywhere/litestar/__init__.py b/sqlspec/adapters/sqlanywhere/litestar/__init__.py new file mode 100644 index 000000000..d4b7031a6 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/litestar/__init__.py @@ -0,0 +1,5 @@ +"""Litestar integration for SAP SQL Anywhere adapter.""" + +from sqlspec.adapters.sqlanywhere.litestar.store import SqlanywhereStore + +__all__ = ("SqlanywhereStore",) diff --git a/sqlspec/adapters/sqlanywhere/litestar/store.py b/sqlspec/adapters/sqlanywhere/litestar/store.py new file mode 100644 index 000000000..e2b325534 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/litestar/store.py @@ -0,0 +1,215 @@ +"""SAP SQL Anywhere Litestar Store implementation.""" + +from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING, Any + +from sqlspec.adapters.sqlanywhere._typing import SqlanywhereCursor +from sqlspec.adapters.sqlanywhere.core import quote_double_identifier, split_schema_and_table +from sqlspec.extensions.litestar.store import BaseSQLSpecStore +from sqlspec.utils.sync_tools import async_ + +if TYPE_CHECKING: + from sqlspec.adapters.sqlanywhere.config import SqlanywhereConfig + +__all__ = ("SqlanywhereStore",) + + +class SqlanywhereStore(BaseSQLSpecStore["SqlanywhereConfig"]): + """SAP SQL Anywhere-backed session store using sqlanydb sync sessions.""" + + __slots__ = () + + def __init__(self, config: "SqlanywhereConfig") -> None: + super().__init__(config) + + async def create_table(self) -> None: + """Create the session table if it doesn't exist.""" + if not self.create_schema_enabled: + await self.reconcile_schema() + return + await async_(self._create_table)() + await self.reconcile_schema(assume_existing=True) + + async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + """Get a session value by key.""" + return await async_(self._get)(key, renew_for) + + async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None: + """Store a session value.""" + await async_(self._set)(key, value, expires_in) + + async def delete(self, key: str) -> None: + """Delete a session by key.""" + await async_(self._delete)(key) + + async def delete_all(self) -> None: + """Delete all sessions from the store.""" + await async_(self._delete_all)() + + async def exists(self, key: str) -> bool: + """Check if a session key exists and is not expired.""" + return await async_(self._exists)(key) + + async def expires_in(self, key: str) -> "int | None": + """Get the time in seconds until the session expires.""" + return await async_(self._expires_in)(key) + + async def delete_expired(self) -> int: + """Delete all expired sessions.""" + return await async_(self._delete_expired)() + + def _table_ddl(self) -> str: + """Get SQL Anywhere CREATE TABLE IF NOT EXISTS DDL.""" + _, table_name = split_schema_and_table(self._table_name) + table_ref = _table_ref(self._table_name) + index_name = quote_double_identifier(f"IX_{table_name}_expires_at") + return ( + f"CREATE TABLE IF NOT EXISTS {table_ref} (" + "session_id VARCHAR(255) NOT NULL PRIMARY KEY, " + "data LONG BINARY NOT NULL, " + "expires_at TIMESTAMP NULL, " + "created_at TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL, " + "updated_at TIMESTAMP DEFAULT CURRENT UTC TIMESTAMP NOT NULL" + ");\n" + f"CREATE INDEX IF NOT EXISTS {index_name} ON {table_ref}(expires_at);" + ) + + def _drop_table_sql(self) -> "list[str]": + """Get SQL Anywhere DROP TABLE IF EXISTS statements.""" + table_ref = _table_ref(self._table_name) + return [f"DROP TABLE IF EXISTS {table_ref};"] + + def _create_table(self) -> None: + with self._config.provide_session() as driver: + driver.execute_script(self._table_ddl()) + driver.commit() + self._log_table_created() + + def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + table_ref = _table_ref(self._table_name) + sql = f""" + SELECT data, expires_at FROM {table_ref} + WHERE session_id = ? + AND (expires_at IS NULL OR expires_at > CURRENT UTC TIMESTAMP) + """ + with self._config.provide_connection() as conn: + with SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, (key,)) + row = cursor.fetchone() + + if row is None: + return None + + expires_at = _normalize_utc(_row_value(row, "expires_at", 1)) + if renew_for is not None and expires_at is not None: + new_expires_at = self._calculate_expires_at(renew_for) + if new_expires_at is not None: + with SqlanywhereCursor(conn) as update_cursor: + update_cursor.execute( + f""" + UPDATE {table_ref} + SET expires_at = ?, updated_at = CURRENT UTC TIMESTAMP + WHERE session_id = ? + """, + (new_expires_at, key), + ) + conn.commit() + + return _coerce_bytes(_row_value(row, "data", 0)) + + def _set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None: + data = self._value_to_bytes(value) + expires_at = self._calculate_expires_at(expires_in) + table_ref = _table_ref(self._table_name) + sql = f""" + INSERT INTO {table_ref} (session_id, data, expires_at, updated_at) + ON EXISTING UPDATE + VALUES (?, ?, ?, CURRENT UTC TIMESTAMP) + """ + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, (key, data, expires_at)) + conn.commit() + + def _delete(self, key: str) -> None: + table_ref = _table_ref(self._table_name) + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(f"DELETE FROM {table_ref} WHERE session_id = ?", (key,)) + conn.commit() + + def _delete_all(self) -> None: + table_ref = _table_ref(self._table_name) + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(f"TRUNCATE TABLE {table_ref}") + conn.commit() + self._log_delete_all() + + def _exists(self, key: str) -> bool: + table_ref = _table_ref(self._table_name) + sql = f""" + SELECT 1 FROM {table_ref} + WHERE session_id = ? + AND (expires_at IS NULL OR expires_at > CURRENT UTC TIMESTAMP) + """ + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, (key,)) + return cursor.fetchone() is not None + + def _expires_in(self, key: str) -> "int | None": + table_ref = _table_ref(self._table_name) + sql = f""" + SELECT expires_at FROM {table_ref} + WHERE session_id = ? + """ + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql, (key,)) + row = cursor.fetchone() + + if row is None: + return None + + expires_at = _normalize_utc(_row_value(row, "expires_at", 0)) + if expires_at is None: + return None + + now = datetime.now(UTC) + if expires_at <= now: + return 0 + return int((expires_at - now).total_seconds()) + + def _delete_expired(self) -> int: + table_ref = _table_ref(self._table_name) + sql = f"DELETE FROM {table_ref} WHERE expires_at <= CURRENT UTC TIMESTAMP" + with self._config.provide_connection() as conn, SqlanywhereCursor(conn) as cursor: + cursor.execute(sql) + conn.commit() + count = cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0 + if count > 0: + self._log_delete_expired(count) + return count + + +def _table_ref(table_name: str) -> str: + schema_name, bare_table_name = split_schema_and_table(table_name) + return f"{quote_double_identifier(schema_name)}.{quote_double_identifier(bare_table_name)}" + + +def _row_value(row: Any, key: str, index: int) -> Any: + if isinstance(row, dict): + return row.get(key) + return row[index] + + +def _coerce_bytes(value: Any) -> bytes: + if isinstance(value, bytes): + return value + if isinstance(value, (bytearray, memoryview)): + return bytes(value) + return str(value).encode("utf-8") + + +def _normalize_utc(value: "datetime | None") -> "datetime | None": + if value is None: + return None + if value.tzinfo is None: + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) diff --git a/sqlspec/adapters/sqlanywhere/migrations.py b/sqlspec/adapters/sqlanywhere/migrations.py new file mode 100644 index 000000000..c85c3b501 --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/migrations.py @@ -0,0 +1,168 @@ +"""SAP SQL Anywhere-specific migration tracker.""" + +import logging +import os +from contextlib import suppress +from typing import TYPE_CHECKING + +from sqlspec.adapters.sqlanywhere.core import ( + escape_sql_literal, + format_identifier, + quote_double_identifier, + split_schema_and_table, +) +from sqlspec.builder import CreateTable, sql +from sqlspec.migrations.tracker import SyncMigrationTracker +from sqlspec.migrations.version import parse_version +from sqlspec.observability import resolve_db_system +from sqlspec.utils.logging import get_logger, log_with_context + +if TYPE_CHECKING: + from sqlspec.driver import SyncDriverAdapterBase + +__all__ = ("SqlanywhereMigrationTrackerMixin", "SqlanywhereSyncMigrationTracker") + +logger = get_logger("sqlspec.migrations.sqlanywhere") + + +class SqlanywhereMigrationTrackerMixin: + """SAP SQL Anywhere-specific migration table DDL and schema maintenance.""" + + __slots__ = () + + version_table: str + + def _tracking_table_ddl(self) -> CreateTable: + """Return SQL Anywhere-compatible migration tracking table DDL.""" + return ( + sql + .create_table(self.version_table) + .if_not_exists() + .column("version_num", "VARCHAR(32)", primary_key=True) + .column("version_type", "VARCHAR(16)") + .column("execution_sequence", "INTEGER") + .column("description", "LONG VARCHAR") + .column("applied_at", "TIMESTAMP", default="CURRENT UTC TIMESTAMP", not_null=True) + .column("execution_time_ms", "INTEGER") + .column("checksum", "VARCHAR(64)") + .column("applied_by", "VARCHAR(255)") + .column("replaces", "LONG VARCHAR") + ) + + def _idempotent_tracking_table_ddl_text(self) -> str: + """Render ``CREATE TABLE IF NOT EXISTS`` DDL with SQL Anywhere native column types.""" + column_lines: list[str] = [] + for column_def in self._tracking_table_ddl().columns: + default_clause = f" DEFAULT {column_def.default}" if column_def.default else "" + null_clause = " NOT NULL" if column_def.primary_key or column_def.not_null else " NULL" + primary_key_clause = " PRIMARY KEY" if column_def.primary_key else "" + column_lines.append( + f" {quote_double_identifier(column_def.name)} {column_def.dtype}{default_clause}{null_clause}{primary_key_clause}" + ) + return ( + f"CREATE TABLE IF NOT EXISTS {format_identifier(self.version_table)} (\n" + + ",\n".join(column_lines) + + "\n);" + ) + + def _existing_columns_query(self) -> str: + """Return SQL Anywhere catalog query text for migration tracking table columns.""" + schema_name, table_name = split_schema_and_table(self.version_table) + return f""" + SELECT c.column_name AS column_name + FROM SYS.SYSTABCOL c + INNER JOIN SYS.SYSTAB t ON t.table_id = c.table_id + INNER JOIN SYS.SYSUSER u ON u.user_id = t.creator + WHERE t.table_name = '{escape_sql_literal(table_name)}' + AND u.user_name = '{escape_sql_literal(schema_name)}' + """ + + def _add_column_statement_text(self, column_name: str) -> str | None: + """Return SQL Anywhere ALTER TABLE text for a missing migration column.""" + target_create = self._tracking_table_ddl() + column_def = next((col for col in target_create.columns if col.name.lower() == column_name), None) + if column_def is None: + return None + default_clause = f" DEFAULT {column_def.default}" if column_def.default else "" + null_clause = " NOT NULL" if column_def.not_null else " NULL" + return ( + f"ALTER TABLE {format_identifier(self.version_table)} " + f"ADD {quote_double_identifier(column_def.name)} {column_def.dtype}{default_clause}{null_clause}" + ) + + +class SqlanywhereSyncMigrationTracker(SqlanywhereMigrationTrackerMixin, SyncMigrationTracker): + """Sync migration tracker using SAP SQL Anywhere DDL.""" + + def ensure_tracking_table(self, driver: "SyncDriverAdapterBase") -> None: + """Create the migration tracking table if it does not exist.""" + driver.execute_script(self._idempotent_tracking_table_ddl_text()) + self._commit_if_needed(driver) + self._migrate_schema_if_needed(driver) + + def record_migration( + self, + driver: "SyncDriverAdapterBase", + version: str, + description: str, + execution_time_ms: int, + checksum: str, + *, + commit: bool = True, + ) -> None: + """Record a successfully applied migration with SQL Anywhere-compatible metadata.""" + parsed_version = parse_version(version) + version_type = parsed_version.type.value + result = driver.execute(self._next_execution_sequence_query()) + next_sequence = result.get_data()[0]["next_seq"] if result.data else 1 + driver.execute( + self._record_migration_statement( + version, + version_type, + next_sequence, + description, + execution_time_ms, + checksum, + os.environ.get("USER", "unknown"), + ) + ) + if commit: + self._commit_if_needed(driver) + + def _commit_if_needed(self, driver: "SyncDriverAdapterBase") -> None: + if hasattr(driver, "driver_features"): + self._safe_commit(driver) + return + driver.commit() + + def _migrate_schema_if_needed(self, driver: "SyncDriverAdapterBase") -> None: + """Check and add missing tracking table columns through SQL Anywhere catalog tables.""" + try: + rows = driver.select(self._existing_columns_query()) + existing_columns = {str(row["column_name"]).lower() for row in rows if row.get("column_name") is not None} + missing_columns = self._detect_missing_columns(existing_columns) + if not missing_columns: + return + for column_name in sorted(missing_columns): + self._add_column(driver, column_name) + self._commit_if_needed(driver) + except Exception as exc: + with suppress(Exception): + driver.rollback() + log_with_context( + logger, + logging.ERROR, + "migration.track", + db_system=resolve_db_system(type(driver).__name__), + table=self.version_table, + operation="schema_check", + status="failed", + error_type=type(exc).__name__, + ) + + def _add_column(self, driver: "SyncDriverAdapterBase", column_name: str) -> None: + """Add a single missing migration tracking column.""" + add_column_sql = self._add_column_statement_text(column_name) + if add_column_sql is None: + return + driver.execute_script(add_column_sql) diff --git a/sqlspec/adapters/sqlanywhere/pool.py b/sqlspec/adapters/sqlanywhere/pool.py new file mode 100644 index 000000000..db1d4effb --- /dev/null +++ b/sqlspec/adapters/sqlanywhere/pool.py @@ -0,0 +1,237 @@ +"""SAP SQL Anywhere thread-safe connection pool.""" + +import contextlib +import logging +import threading +import time +from contextlib import contextmanager +from importlib import import_module +from typing import TYPE_CHECKING, Any, cast + +from sqlspec.adapters.sqlanywhere._typing import SqlanywhereConnection +from sqlspec.exceptions import MissingDependencyError +from sqlspec.utils.logging import POOL_LOGGER_NAME, get_logger, log_with_context +from sqlspec.utils.uuids import uuid4 + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + +__all__ = ("SqlanywhereConnectionPool",) + +logger = get_logger(POOL_LOGGER_NAME) +_ADAPTER_NAME = "sqlanywhere" + + +def _load_sqlanydb_module() -> Any: + """Import ``sqlanydb`` lazily or raise ``MissingDependencyError``.""" + try: + return import_module("sqlanydb") + except ImportError as exc: + raise MissingDependencyError(package="sqlanydb", install_package="sqlanywhere") from exc + + +class SqlanywhereConnectionPool: + """Thread-local connection manager for SAP SQL Anywhere over sqlanydb.""" + + __slots__ = ( + "_checked_out", + "_connection_parameters", + "_connection_registry", + "_connection_threads", + "_generation", + "_health_check_interval", + "_max_size", + "_min_size", + "_on_connection_create", + "_pool_id", + "_recycle_seconds", + "_registry_lock", + "_thread_local", + "_timeout", + ) + + def __init__( + self, + connection_parameters: "dict[str, Any]", + min_size: int = 1, + max_size: int = 10, + timeout: float = 30.0, + recycle_seconds: int = 86400, + health_check_interval: float = 30.0, + on_connection_create: "Callable[[SqlanywhereConnection], None] | None" = None, + ) -> None: + self._connection_parameters = dict(connection_parameters) + self._min_size = min_size + self._max_size = max_size + self._timeout = timeout + self._thread_local = threading.local() + self._connection_registry: set[SqlanywhereConnection] = set() + self._connection_threads: dict[SqlanywhereConnection, threading.Thread] = {} + self._checked_out = 0 + self._generation = 0 + self._registry_lock = threading.Lock() + self._recycle_seconds = recycle_seconds + self._health_check_interval = health_check_interval + self._on_connection_create = on_connection_create + self._pool_id = str(uuid4())[:8] + + @property + def _database_name(self) -> str: + return str(self._connection_parameters.get("dbn", self._connection_parameters.get("eng", "unknown"))) + + def new_connection(self) -> SqlanywhereConnection: + """Open a standalone SAP SQL Anywhere connection via ``sqlanydb.connect``.""" + sqlanydb = _load_sqlanydb_module() + connection = sqlanydb.connect(**self._connection_parameters) + if self._on_connection_create is not None: + self._on_connection_create(connection) + return cast("SqlanywhereConnection", connection) + + @contextmanager + def get_connection(self) -> "Generator[SqlanywhereConnection, None, None]": + """Yield a thread-local SQL Anywhere connection.""" + connection = self.acquire() + try: + yield connection + except Exception: + with contextlib.suppress(Exception): + self._close_thread_connection() + raise + finally: + self.release(connection) + + def close(self) -> None: + """Close every connection opened by this pool.""" + self._close_thread_connection() + with self._registry_lock: + orphaned = list(self._connection_registry) + self._connection_registry.clear() + self._connection_threads.clear() + self._checked_out = 0 + self._generation += 1 + for connection in orphaned: + with contextlib.suppress(Exception): + connection.close() + + def acquire(self) -> SqlanywhereConnection: + """Acquire a healthy thread-local connection.""" + connection = self._get_thread_connection() + with self._registry_lock: + self._checked_out += 1 + return connection + + def release(self, connection: SqlanywhereConnection) -> None: + """Release a checked-out connection back to the thread-local slot.""" + _ = connection + with self._registry_lock: + if self._checked_out > 0: + self._checked_out -= 1 + + def size(self) -> int: + """Return the number of active connections registered with this pool.""" + self._prune_dead_thread_connections() + with self._registry_lock: + return len(self._connection_registry) + + def checked_out(self) -> int: + """Return the number of currently checked-out connections.""" + with self._registry_lock: + return self._checked_out + + def _prune_dead_thread_connections(self) -> None: + with self._registry_lock: + dead = [conn for conn, thread in self._connection_threads.items() if not thread.is_alive()] + for conn in dead: + self._connection_registry.discard(conn) + self._connection_threads.pop(conn, None) + for conn in dead: + with contextlib.suppress(Exception): + conn.close() + + def _create_connection(self) -> SqlanywhereConnection: + self._prune_dead_thread_connections() + connection = self.new_connection() + with self._registry_lock: + self._connection_registry.add(connection) + self._connection_threads[connection] = threading.current_thread() + return connection + + def _is_connection_alive(self, connection: SqlanywhereConnection) -> bool: + cursor = None + try: + cursor = connection.cursor() + cursor.execute("SELECT 1") + cursor.fetchone() + except Exception: + return False + else: + return True + finally: + if cursor is not None and hasattr(cursor, "close"): + with contextlib.suppress(Exception): + cursor.close() + + def _get_thread_connection(self) -> SqlanywhereConnection: + now = time.time() + current_gen = self._generation + conn = getattr(self._thread_local, "connection", None) + created_at = getattr(self._thread_local, "created_at", 0.0) + last_used = getattr(self._thread_local, "last_used", 0.0) + conn_gen = getattr(self._thread_local, "generation", -1) + + if conn is not None and conn_gen != current_gen: + self._close_thread_connection() + conn = None + + if conn is None: + conn = self._create_connection() + self._thread_local.connection = conn + self._thread_local.created_at = now + self._thread_local.last_used = now + self._thread_local.generation = current_gen + return conn + + if self._recycle_seconds > 0 and now - created_at >= self._recycle_seconds: + log_with_context( + logger, + logging.DEBUG, + "pool.connection.recycle", + adapter_name=_ADAPTER_NAME, + pool_id=self._pool_id, + database=self._database_name, + reason="exceeded_recycle_time", + ) + self._close_thread_connection() + conn = self._create_connection() + self._thread_local.connection = conn + self._thread_local.created_at = now + self._thread_local.last_used = now + self._thread_local.generation = current_gen + return conn + + if ( + self._health_check_interval > 0 + and now - last_used >= self._health_check_interval + and not self._is_connection_alive(conn) + ): + self._close_thread_connection() + conn = self._create_connection() + self._thread_local.connection = conn + self._thread_local.created_at = now + self._thread_local.generation = current_gen + + self._thread_local.last_used = now + return cast("SqlanywhereConnection", conn) + + def _close_thread_connection(self) -> None: + conn = getattr(self._thread_local, "connection", None) + if conn is not None: + with self._registry_lock: + self._connection_registry.discard(conn) + self._connection_threads.pop(conn, None) + with contextlib.suppress(Exception): + conn.close() + self._thread_local.connection = None + self._thread_local.created_at = 0.0 + self._thread_local.last_used = 0.0 + self._thread_local.generation = -1 diff --git a/tests/unit/adapters/test_sqlanywhere.py b/tests/unit/adapters/test_sqlanywhere.py new file mode 100644 index 000000000..8962f072a --- /dev/null +++ b/tests/unit/adapters/test_sqlanywhere.py @@ -0,0 +1,489 @@ +"""Unit tests for the SAP SQL Anywhere (sqlanydb) adapter.""" + +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from sqlspec.adapters.sqlanywhere import ( + DEFAULT_SQLANYWHERE_PORT, + SqlanywhereConfig, + SqlanywhereConnection, + SqlanywhereConnectionPool, + SqlanywhereCursor, + SqlanywhereDataDictionary, + SqlanywhereDriver, + SqlanywhereExceptionHandler, + SqlanywhereSyncDataDictionary, + build_connection_config, + create_mapped_exception, + default_statement_config, + driver_profile, +) +from sqlspec.adapters.sqlanywhere.adk import SqlanywhereADKConfig, SqlanywhereADKMemoryStore, SqlanywhereADKStore +from sqlspec.adapters.sqlanywhere.events import SqlanywhereEventQueueStore, SqlanywhereSyncEventQueueStore +from sqlspec.adapters.sqlanywhere.litestar import SqlanywhereStore +from sqlspec.adapters.sqlanywhere.migrations import SqlanywhereSyncMigrationTracker +from sqlspec.core import SQL, ParameterStyle +from sqlspec.data_dictionary.dialects.sqlanywhere import SqlanywhereVersionInfo +from sqlspec.exceptions import ( + CheckViolationError, + DatabaseConnectionError, + DataError, + DeadlockError, + ForeignKeyViolationError, + MissingDependencyError, + NotFoundError, + NotNullViolationError, + OperationalError, + PermissionDeniedError, + SerializationConflictError, + SQLParsingError, + UniqueViolationError, +) + + +class FakeSqlanydbError(Exception): + """Fake sqlanydb Error for SQL Anywhere unit tests.""" + + +class FakeCursor: + """Minimal DB-API 2.0 cursor for SQL Anywhere unit tests.""" + + def __init__( + self, rows: "list[Any] | None" = None, description: "list[tuple[str, ...]] | None" = None, rowcount: int = -1 + ) -> None: + self.rows = rows or [] + self.description = description + self.rowcount = rowcount + self.closed = False + self.calls: list[tuple[str, Any]] = [] + self.many_calls: list[tuple[str, Any]] = [] + self.fetchmany_sizes: list[int] = [] + + def execute(self, sql: str, parameters: Any = None) -> None: + self.calls.append((sql, parameters)) + + def executemany(self, sql: str, parameters: Any = None) -> None: + self.many_calls.append((sql, parameters)) + + def fetchall(self) -> "list[Any]": + return self.rows + + def fetchmany(self, size: int) -> "list[Any]": + self.fetchmany_sizes.append(size) + chunk = self.rows[:size] + self.rows = self.rows[size:] + return chunk + + def fetchone(self) -> Any: + return self.rows[0] if self.rows else None + + def close(self) -> None: + self.closed = True + + +class FakeConnection: + """Minimal sqlanydb connection for SQL Anywhere unit tests.""" + + def __init__(self, cursor: "FakeCursor | None" = None) -> None: + self.cursor_obj = cursor or FakeCursor() + self.closed = False + self.commits = 0 + self.rollbacks = 0 + + def cursor(self, *args: Any, **kwargs: Any) -> FakeCursor: + self.cursor_args = args + self.cursor_kwargs = kwargs + return self.cursor_obj + + def commit(self) -> None: + self.commits += 1 + + def rollback(self) -> None: + self.rollbacks += 1 + + def close(self) -> None: + self.closed = True + + +def test_build_connection_config_normalizes_friendly_keys_and_strips_pool_keys() -> None: + """build_connection_config maps friendly keys into sqlanydb connection keys and strips pool parameters.""" + cfg = build_connection_config({ + "username": "dba", + "password": "sql", + "database": "demo", + "server": "demo17", + "database_file": "/var/sa/demo.db", + "host": "sa.internal", + "pool_min_size": 2, + "pool_max_size": 8, + "pool_timeout": 15.0, + "pool_recycle_seconds": 600, + "health_check_interval": 30.0, + "extra": {"cset": "UTF-8", "ignored": None}, + }) + + assert cfg["uid"] == "dba" + assert "username" not in cfg + assert "user" not in cfg + assert cfg["pwd"] == "sql" + assert "password" not in cfg + assert cfg["dbn"] == "demo" + assert "database" not in cfg + assert cfg["eng"] == "demo17" + assert "server" not in cfg + assert cfg["dbf"] == "/var/sa/demo.db" + assert "database_file" not in cfg + assert cfg["host"] == f"sa.internal:{DEFAULT_SQLANYWHERE_PORT}" + assert "port" not in cfg + assert cfg["cset"] == "UTF-8" + assert "pool_min_size" not in cfg + assert "pool_max_size" not in cfg + assert "pool_timeout" not in cfg + assert "pool_recycle_seconds" not in cfg + assert "health_check_interval" not in cfg + assert "extra" not in cfg + + +def test_build_connection_config_combines_host_and_explicit_port() -> None: + """build_connection_config combines host and explicit port into host:port.""" + cfg = build_connection_config({"user": "dba", "pwd": "sql", "host": "10.0.0.5", "port": 2640, "dsn": "SADemo"}) + assert cfg["uid"] == "dba" + assert cfg["pwd"] == "sql" + assert cfg["host"] == "10.0.0.5:2640" + assert "port" not in cfg + assert cfg["dsn"] == "SADemo" + + +def test_sqlanywhere_pool_checkout_release_and_close(monkeypatch: pytest.MonkeyPatch) -> None: + """SqlanywhereConnectionPool manages connection acquisition, release, and pool closure.""" + import sqlspec.adapters.sqlanywhere.pool as pool_module + + captured_kwargs: list[dict[str, Any]] = [] + fake_conn = FakeConnection(FakeCursor(rows=[(1,)], description=[("1",)])) + + class FakeSqlanydbModule: + def connect(self, **kwargs: Any) -> FakeConnection: + captured_kwargs.append(kwargs) + return fake_conn + + monkeypatch.setattr(pool_module, "_load_sqlanydb_module", lambda: FakeSqlanydbModule()) + + cfg = build_connection_config({"user": "dba", "password": "sql", "database": "demo", "host": "localhost"}) + pool = SqlanywhereConnectionPool(cfg, min_size=1, max_size=2) + conn = pool.acquire() + assert cast("object", conn) is fake_conn + assert captured_kwargs[0]["uid"] == "dba" + assert captured_kwargs[0]["pwd"] == "sql" + assert captured_kwargs[0]["dbn"] == "demo" + assert captured_kwargs[0]["host"] == "localhost:2638" + + pool.release(conn) + assert pool.size() == 1 + pool.close() + assert fake_conn.closed is True + + +def test_sqlanywhere_pool_raises_missing_dependency_when_sqlanydb_absent(monkeypatch: pytest.MonkeyPatch) -> None: + """SqlanywhereConnectionPool raises MissingDependencyError when sqlanydb is not installed.""" + import sqlspec.adapters.sqlanywhere.pool as pool_module + + def _raise_import_error() -> Any: + msg = "No module named 'sqlanydb'" + raise ImportError(msg) + + monkeypatch.setattr(pool_module, "import_module", lambda name: _raise_import_error()) + + pool = SqlanywhereConnectionPool({"uid": "dba", "pwd": "sql"}) + with pytest.raises(MissingDependencyError, match="sqlanydb"): + pool.acquire() + + +@pytest.mark.parametrize( + ("args", "expected_type"), + [ + (("23505", -193, "Primary key for table 'users' is not unique"), UniqueViolationError), + (("23505", -196, "Index 'ix_email' for table 'users' would not be unique"), UniqueViolationError), + (("23503", -194, "No primary key value for foreign key 'fk_user'"), ForeignKeyViolationError), + (("23503", -198, "Primary key for row in table 'users' is referenced"), ForeignKeyViolationError), + (("23502", -195, "Column 'email' in table 'users' cannot be NULL"), NotNullViolationError), + (("23506", -209, "Invalid value for column 'age'"), CheckViolationError), + ((None, -1091, "Constraint 'chk_age' violated"), CheckViolationError), + (("40001", -306, "Deadlock detected"), DeadlockError), + ((None, -307, "All threads are blocked"), DeadlockError), + ((None, -684, "Rollback occurred due to deadlock"), DeadlockError), + ((None, -210, "User 'another' has the row in 'users' locked"), SerializationConflictError), + (("42000", -131, "Syntax error near 'SELEC'"), SQLParsingError), + (("42S02", -141, "Table 'missing_tbl' not found"), NotFoundError), + (("42S22", -143, "Column 'missing_col' not found"), NotFoundError), + (("08001", -100, "Database server not found"), DatabaseConnectionError), + (("28000", -103, "Invalid user ID or password"), DatabaseConnectionError), + ((None, -83, "Specified database not found"), DatabaseConnectionError), + ((None, -308, "Connection was terminated"), DatabaseConnectionError), + ((None, -832, "Connection error"), DatabaseConnectionError), + ((None, -101, "Not connected to a database"), OperationalError), + (("22018", -157, "Cannot convert 'abc' to a numeric"), DataError), + (("22003", -158, "Value 999999 out of range for destination"), DataError), + (("42501", -121, "Permission denied: you do not have permission"), PermissionDeniedError), + ], +) +def test_create_mapped_exception_maps_sqlanywhere_sqlcode_and_sqlstate( + args: tuple[Any, ...], expected_type: type[Exception] +) -> None: + """create_mapped_exception maps SQL Anywhere negative SQLCODEs and SQLSTATEs to SQLSpec exceptions.""" + err = FakeSqlanydbError(*args) + mapped = create_mapped_exception(err) + assert isinstance(mapped, expected_type) + + +def test_sqlanywhere_driver_profile_and_statement_config() -> None: + """Sqlanywhere driver profile uses QMARK execution and sqlanywhere dialect.""" + assert driver_profile.name == "sqlanywhere" + assert driver_profile.default_style == ParameterStyle.QMARK + assert driver_profile.default_execution_style == ParameterStyle.QMARK + assert driver_profile.default_dialect == "sqlanywhere" + assert default_statement_config.dialect == "sqlanywhere" + + +def test_sqlanywhere_cursor_context_manager_closes_cursor() -> None: + """SqlanywhereCursor creates and closes the underlying DB-API cursor.""" + fake_cursor = FakeCursor() + fake_conn = FakeConnection(fake_cursor) + with SqlanywhereCursor(cast("SqlanywhereConnection", fake_conn)) as cursor: + assert cursor is fake_cursor + assert fake_cursor.closed is True + + +def test_sqlanywhere_exception_handler_defers_mapped_exception() -> None: + """SqlanywhereExceptionHandler captures DB-API errors into pending_exception.""" + handler = SqlanywhereExceptionHandler() + with handler: + raise FakeSqlanydbError("23505", -193, "Primary key not unique") + assert isinstance(handler.pending_exception, UniqueViolationError) + + +def test_sqlanywhere_driver_execute_many_script_and_transactions() -> None: + """SqlanywhereDriver executes SELECT, DML, execute_many, execute_script, and transactions.""" + select_cursor = FakeCursor(rows=[(1, "Ada")], description=[("id",), ("name",)], rowcount=1) + conn = FakeConnection(select_cursor) + driver = SqlanywhereDriver(cast("SqlanywhereConnection", conn)) + + select_result = driver.execute(SQL("SELECT id, name FROM DBA.users WHERE id = ?", 1)) + assert select_result.get_data() == [{"id": 1, "name": "Ada"}] + assert select_cursor.calls[0][1] == (1,) + + dml_cursor = FakeCursor(rows=[], description=None, rowcount=2) + conn.cursor_obj = dml_cursor + many_result = driver.execute_many(SQL("INSERT INTO DBA.users (id, name) VALUES (?, ?)"), [(1, "Ada"), (2, "Grace")]) + assert many_result.rows_affected == 2 + assert len(dml_cursor.many_calls) == 1 + + script_cursor = FakeCursor(rows=[], description=None, rowcount=1) + conn.cursor_obj = script_cursor + script_result = driver.execute_script("CREATE TABLE IF NOT EXISTS DBA.t1 (id INT); INSERT INTO DBA.t1 VALUES (1);") + assert script_result.total_statements == 2 + + driver.begin() + driver.commit() + assert conn.commits == 1 + + driver.begin() + driver.rollback() + assert conn.rollbacks == 1 + + +def test_sqlanywhere_driver_select_to_arrow() -> None: + """SqlanywhereDriver.select_to_arrow streams rows into a PyArrow table.""" + cursor = FakeCursor(rows=[(1, "Ada"), (2, "Grace")], description=[("id",), ("name",)], rowcount=2) + conn = FakeConnection(cursor) + driver = SqlanywhereDriver(cast("SqlanywhereConnection", conn)) + + arrow_result = driver.select_to_arrow("SELECT id, name FROM DBA.users") + assert arrow_result.rows_affected == 2 + assert arrow_result.to_dict() == [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Grace"}] + + +def test_sqlanywhere_data_dictionary_version_and_metadata() -> None: + """SqlanywhereSyncDataDictionary parses version banners and builds table/column/index/FK metadata.""" + dd = SqlanywhereSyncDataDictionary() + assert SqlanywhereDataDictionary is SqlanywhereSyncDataDictionary + + fake_driver = MagicMock(spec=SqlanywhereDriver) + fake_driver.select_one_or_none.return_value = {"version_string": "17.0.11.7458"} + fake_driver.select_value_or_none.return_value = "DBA" + + def _select_side_effect(query: Any, *args: Any, **kwargs: Any) -> list[dict[str, Any]]: + _ = (args, kwargs) + sql_text = str(query) + if "SYS.SYSTABCOL" in sql_text or "SYS.SYSCOLUMN" in sql_text: + return [ + { + "schema_name": "DBA", + "table_name": "users", + "column_name": "id", + "data_type": "integer", + "is_nullable": 0, + "column_default": "autoincrement", + "ordinal_position": 1, + "max_length": 4, + "numeric_scale": 0, + "is_primary": 1, + "is_unique": 1, + "is_identity": 1, + "identity_generation": "BY DEFAULT", + "is_generated": 0, + }, + { + "schema_name": "DBA", + "table_name": "users", + "column_name": "payload", + "data_type": "long nvarchar", + "is_nullable": 1, + "column_default": None, + "ordinal_position": 2, + "max_length": 2147483647, + "numeric_scale": 0, + "is_primary": 0, + "is_unique": 0, + "is_identity": 0, + "identity_generation": None, + "is_generated": 0, + }, + ] + if "SYS.SYSTAB" in sql_text and "SYS.SYSIDX" not in sql_text: + return [{"schema_name": "DBA", "table_name": "users", "table_type": "BASE TABLE"}] + return [] + + fake_driver.select.side_effect = _select_side_effect + + version = dd.get_version(fake_driver) + assert isinstance(version, SqlanywhereVersionInfo) + assert (version.major, version.minor, version.patch, version.build_number) == (17, 0, 11, "7458") + assert dd.get_feature_flag(fake_driver, "supports_cte") is True + assert dd.get_feature_flag(fake_driver, "supports_merge") is True + assert dd.get_optimal_type(fake_driver, "uuid") == "UNIQUEIDENTIFIER" + assert dd.get_optimal_type(fake_driver, "blob") == "LONG BINARY" + + tables = dd.get_tables(fake_driver, schema="DBA") + assert len(tables) == 1 + assert tables[0]["table_name"] == "users" + + columns = dd.get_columns(fake_driver, table="users", schema="DBA") + assert len(columns) == 2 + assert columns[0]["identity_generation"] == "BY DEFAULT" + assert columns[1]["data_type"] == "long nvarchar" + + +def test_sqlanywhere_migration_tracker_and_extension_stores() -> None: + """SqlanywhereSyncMigrationTracker, SqlanywhereStore, SqlanywhereSyncEventQueueStore, and SqlanywhereADKStore emit SQL Anywhere DDL.""" + tracker = SqlanywhereSyncMigrationTracker(version_table_name="DBA.ddl_migrations") + ddl_text = tracker._idempotent_tracking_table_ddl_text() + assert "CREATE TABLE IF NOT EXISTS" in ddl_text + assert "VARCHAR(32)" in ddl_text + assert "TIMESTAMP" in ddl_text + assert "CURRENT UTC TIMESTAMP" in ddl_text + assert "LONG VARCHAR" in ddl_text + assert "NVARCHAR(MAX)" not in ddl_text + + config = SqlanywhereConfig( + connection_config={"user": "dba", "password": "sql", "database": "demo"}, + extension_config={ + "litestar": {"session_table": "litestar_session", "manage_schema": False}, + "events": {"queue_table": "event_queue"}, + "adk": { + "session_table": "adk_sessions", + "events_table": "adk_events", + "memory_table": "adk_memory", + "owner_id_column": "tenant_id INTEGER NOT NULL REFERENCES DBA.tenants(id)", + }, + }, + ) + + litestar_store = SqlanywhereStore(config) + create_litestar_sql = litestar_store._table_ddl() + assert "CREATE TABLE IF NOT EXISTS" in create_litestar_sql + assert "LONG BINARY" in create_litestar_sql + assert "TIMESTAMP" in create_litestar_sql + drop_litestar_sql = litestar_store._drop_table_sql() + assert "DROP TABLE IF EXISTS" in drop_litestar_sql[0] + + event_store = SqlanywhereSyncEventQueueStore(config) + assert SqlanywhereEventQueueStore is SqlanywhereSyncEventQueueStore + event_statements = event_store.create_statements() + assert "LONG VARCHAR" in event_statements[0] + assert "TIMESTAMP" in event_statements[0] + assert "CURRENT UTC TIMESTAMP" in event_statements[0] + + adk_store = SqlanywhereADKStore(config) + assert isinstance(adk_store.config, SqlanywhereConfig) + session_ddl = adk_store._sessions_table_ddl() + events_ddl = adk_store._events_table_ddl() + assert "CREATE TABLE IF NOT EXISTS" in session_ddl + assert "LONG VARCHAR" in session_ddl + assert "tenant_id INTEGER NOT NULL REFERENCES DBA.tenants(id)" in session_ddl + assert "LONG VARCHAR" in events_ddl + assert "LONG BINARY" in events_ddl + assert "BIT NULL" in events_ddl + + memory_store = SqlanywhereADKMemoryStore(config) + memory_ddl = memory_store._memory_table_ddl() + assert "CREATE TABLE IF NOT EXISTS" in memory_ddl + assert "LONG VARCHAR" in memory_ddl + assert "TIMESTAMP" in memory_ddl + assert isinstance(SqlanywhereADKConfig(), dict) + + +@pytest.mark.anyio +async def test_sqlanywhere_litestar_and_adk_stores_roundtrip_with_mocked_connection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """SqlanywhereStore, SqlanywhereADKStore, and SqlanywhereADKMemoryStore execute operations cleanly.""" + config = SqlanywhereConfig(connection_config={"user": "dba", "password": "sql", "database": "demo"}) + litestar_store = SqlanywhereStore(config) + + monkeypatch.setattr(SqlanywhereStore, "_get", MagicMock(return_value=b"payload")) + monkeypatch.setattr(SqlanywhereStore, "_set", MagicMock()) + monkeypatch.setattr(SqlanywhereStore, "_delete", MagicMock()) + monkeypatch.setattr(SqlanywhereStore, "_exists", MagicMock(return_value=True)) + monkeypatch.setattr(SqlanywhereStore, "_expires_in", MagicMock(return_value=90)) + monkeypatch.setattr(SqlanywhereStore, "_delete_expired", MagicMock(return_value=3)) + + await litestar_store.set("sess-1", "payload", expires_in=90) + assert await litestar_store.get("sess-1") == b"payload" + assert await litestar_store.exists("sess-1") is True + assert await litestar_store.expires_in("sess-1") == 90 + assert await litestar_store.delete_expired() == 3 + await litestar_store.delete("sess-1") + + captured: list[tuple[str, tuple[Any, ...]]] = [] + + def _capture(_self: SqlanywhereADKStore, sql: str, params: "tuple[Any, ...]" = ()) -> "list[Any]": + captured.append((sql, tuple(params))) + return [] + + monkeypatch.setattr(SqlanywhereADKStore, "_execute_fetchall", _capture) + adk_store = SqlanywhereADKStore(config) + adk_store.list_sessions("app", "u1", order_by="create_time", descending=False, limit=10, offset=20) + sql, params = captured[0] + normalized_sql = " ".join(sql.split()) + assert "ORDER BY create_time ASC, id ASC ROWS LIMIT ? OFFSET ?" in normalized_sql + assert params == ("app", "u1", 10, 20) + + monkeypatch.setattr(SqlanywhereADKMemoryStore, "insert_memory_entries", MagicMock(return_value=1)) + memory_store = SqlanywhereADKMemoryStore(config) + assert memory_store.insert_memory_entries([]) == 1 + + +def test_sqlanywhere_cursor_handles_cursor_without_close() -> None: + """SqlanywhereCursor safely handles raw cursors that lack a close() method.""" + + class CursorWithoutClose: + pass + + class ConnWithBareCursor: + def cursor(self) -> CursorWithoutClose: + return CursorWithoutClose() + + with SqlanywhereCursor(cast("SqlanywhereConnection", ConnWithBareCursor())) as cursor: + assert isinstance(cursor, CursorWithoutClose) From 4d438bf303a0f75bb22704a96542891ff28b95be Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Fri, 30 Oct 2026 18:40:53 +0000 Subject: [PATCH 28/35] build(mypyc): register sybase and sqlanywhere hot-path modules in mypyc and import gates --- pyproject.toml | 11 +++ sqlspec/adapters/sqlanywhere/_typing.py | 15 ++-- sqlspec/adapters/sybase/_typing.py | 15 ++-- sqlspec/adapters/sybase/driver.py | 4 +- sqlspec/adapters/sybase/pool.py | 15 +++- tests/unit/test_mypyc_sybase_surface.py | 104 ++++++++++++++++++++++++ tools/scripts/mypyc_inventory.py | 36 ++++++++ tools/scripts/mypyc_smoke.py | 6 ++ 8 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 tests/unit/test_mypyc_sybase_surface.py diff --git a/pyproject.toml b/pyproject.toml index 7737cd7f2..0ad4f0788 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,9 @@ pymssql = ["pymssql"] pymysql = ["pymysql"] sanic = ["sanic"] spanner = ["google-cloud-spanner>=3.64.0"] +sqlanywhere = ["sqlanydb>=1.0.14"] starlette = ["starlette"] +sybase = ["pymssql"] uuid = ["uuid-utils>=0.17.0"] [dependency-groups] @@ -156,6 +158,8 @@ pg_textsearch = "sqlspec.dialects.postgres:PGTextSearch" pgvector = "sqlspec.dialects.postgres:PGVector" spangres = "sqlspec.dialects.spanner:Spangres" spanner = "sqlspec.dialects.spanner:Spanner" +sqlanywhere = "sqlspec.dialects.sybase:SQLAnywhere" +sybase = "sqlspec.dialects.sybase:Sybase" [tool.uv] exclude-newer = "7 days" @@ -260,6 +264,9 @@ include = [ "sqlspec/dialects/db2/_generators.py", # Db2 custom dialect render hooks "sqlspec/dialects/db2/_parsers.py", # Db2 custom dialect parser hooks "sqlspec/dialects/db2/_transforms.py", # Db2 custom dialect transform helpers + "sqlspec/dialects/sybase/_generators.py", # Sybase and SQL Anywhere custom dialect render hooks + "sqlspec/dialects/sybase/_parsers.py", # Sybase and SQL Anywhere custom dialect parser hooks + "sqlspec/dialects/sybase/_transforms.py", # Sybase and SQL Anywhere custom dialect transform helpers "sqlspec/storage/registry.py", # Safe storage registry/runtime routing "sqlspec/storage/errors.py", # Safe storage error normalization "sqlspec/storage/_paths.py", # Pure storage path handling @@ -484,6 +491,8 @@ markers = [ "mysql_connector: marks tests using mysql-connector", "pymysql: marks tests using pymysql", "psqlpy: marks tests using psqlpy", + "sqlanywhere: marks tests using the sqlanywhere adapter", + "sybase: marks tests using the sybase adapter", ] python_files = ["test_*.py"] testpaths = ["tests"] @@ -539,6 +548,8 @@ module = [ "ibm_db.*", "ibm_db_dbi", "ibm_db_dbi.*", + "sqlanydb", + "sqlanydb.*", "sqlglot", "sqlglot.*", ] diff --git a/sqlspec/adapters/sqlanywhere/_typing.py b/sqlspec/adapters/sqlanywhere/_typing.py index 11e8b6c35..03d4d6450 100644 --- a/sqlspec/adapters/sqlanywhere/_typing.py +++ b/sqlspec/adapters/sqlanywhere/_typing.py @@ -9,16 +9,11 @@ from mypy_extensions import mypyc_attr -from sqlspec.utils.module_loader import import_optional, import_optional_attr +from sqlspec.utils.module_loader import import_optional_attr -SQLANYDB_MODULE = import_optional("sqlanydb") +SQLANYDB_MODULE: Any = None _DRIVER_CLS: Any = None - -class _SqlanywhereFallbackError(Exception): - """Fallback exception base when sqlanydb is not installed.""" - - if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType @@ -33,10 +28,10 @@ class _SqlanywhereFallbackError(Exception): SqlanywhereError: TypeAlias = Exception if not TYPE_CHECKING: - SqlanywhereConnection = import_optional_attr("sqlanydb", "Connection") or Any - SqlanywhereRawCursor = import_optional_attr("sqlanydb", "Cursor") or Any + SqlanywhereConnection = Any + SqlanywhereRawCursor = Any SqlanywhereQueryParams = Any - SqlanywhereError = import_optional_attr("sqlanydb", "Error") or _SqlanywhereFallbackError + SqlanywhereError = Exception __all__ = ( "SQLANYDB_MODULE", diff --git a/sqlspec/adapters/sybase/_typing.py b/sqlspec/adapters/sybase/_typing.py index 2d2013d4f..179925730 100644 --- a/sqlspec/adapters/sybase/_typing.py +++ b/sqlspec/adapters/sybase/_typing.py @@ -7,15 +7,11 @@ import contextlib from typing import TYPE_CHECKING, Any -import pymssql as _pymssql from mypy_extensions import mypyc_attr -from pymssql import Connection as _SybaseConnection -from pymssql import Cursor as _SybaseRawCursor -from pymssql import Error as SybaseError from sqlspec.utils.module_loader import import_optional_attr -PYMSSQL_MODULE = _pymssql +PYMSSQL_MODULE: Any = None _DRIVER_CLS: Any = None if TYPE_CHECKING: @@ -23,6 +19,9 @@ from types import TracebackType from typing import TypeAlias + from pymssql import Connection as _SybaseConnection + from pymssql import Cursor as _SybaseRawCursor + from pymssql import Error as _SybaseError from pymssql._pymssql import QueryParams as SybaseQueryParams from sqlspec.adapters.sybase.driver import SybaseDriver @@ -30,11 +29,13 @@ SybaseConnection: TypeAlias = _SybaseConnection SybaseRawCursor: TypeAlias = _SybaseRawCursor + SybaseError: TypeAlias = _SybaseError if not TYPE_CHECKING: SybaseQueryParams = Any - SybaseConnection = _SybaseConnection - SybaseRawCursor = _SybaseRawCursor + SybaseConnection = Any + SybaseRawCursor = Any + SybaseError = Exception __all__ = ( "PYMSSQL_MODULE", diff --git a/sqlspec/adapters/sybase/driver.py b/sqlspec/adapters/sybase/driver.py index 1ee49ea2b..1d6bf2885 100644 --- a/sqlspec/adapters/sybase/driver.py +++ b/sqlspec/adapters/sybase/driver.py @@ -61,8 +61,8 @@ class SybaseExceptionHandler(BaseSyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: if exc_type is None: return False - if isinstance(exc_val, SybaseError): - self.pending_exception = create_mapped_exception(cast("Exception", exc_val), logger=logger) + if isinstance(exc_val, (SybaseError, Exception)) and not isinstance(exc_val, SQLSpecError): + self.pending_exception = create_mapped_exception(exc_val, logger=logger) return True return False diff --git a/sqlspec/adapters/sybase/pool.py b/sqlspec/adapters/sybase/pool.py index 9dfc43a4f..b97db7e9c 100644 --- a/sqlspec/adapters/sybase/pool.py +++ b/sqlspec/adapters/sybase/pool.py @@ -6,10 +6,12 @@ import threading import time from contextlib import contextmanager +from importlib import import_module from typing import TYPE_CHECKING, Any, cast from sqlspec.adapters.sybase._typing import PYMSSQL_MODULE, SybaseConnection from sqlspec.adapters.sybase.core import DEFAULT_SYBASE_CONN_PROPERTIES, DEFAULT_SYBASE_TDS_VERSION +from sqlspec.exceptions import MissingDependencyError from sqlspec.utils.logging import POOL_LOGGER_NAME, get_logger, log_with_context from sqlspec.utils.uuids import uuid4 @@ -24,6 +26,14 @@ pymssql = PYMSSQL_MODULE +def _load_pymssql_module() -> Any: + """Import ``pymssql`` lazily or raise ``MissingDependencyError``.""" + try: + return import_module("pymssql") + except ImportError as exc: + raise MissingDependencyError(package="pymssql", install_package="sybase") from exc + + class SybaseConnectionPool: """Thread-local connection manager for Sybase ASE over pymssql (FreeTDS TDS 5.0).""" @@ -90,11 +100,14 @@ def new_connection(self) -> SybaseConnection: Returns: SybaseConnection: A newly opened, fully configured connection. """ + module = pymssql + if module is None: + module = _load_pymssql_module() with _TDSVER_LOCK: prior_tdsver = os.environ.get("TDSVER") os.environ["TDSVER"] = self._tds_version try: - connection = pymssql.connect(**self._connection_parameters) + connection = module.connect(**self._connection_parameters) finally: if prior_tdsver is None: os.environ.pop("TDSVER", None) diff --git a/tests/unit/test_mypyc_sybase_surface.py b/tests/unit/test_mypyc_sybase_surface.py new file mode 100644 index 000000000..2575c641f --- /dev/null +++ b/tests/unit/test_mypyc_sybase_surface.py @@ -0,0 +1,104 @@ +"""Unit tests for Sybase and SQL Anywhere packaging, MyPyC inventory, and import boundaries.""" + +import subprocess +import sys +import tomllib +from pathlib import Path + +from tools.scripts.mypyc_inventory import build_inventory, validate_inventory +from tools.scripts.mypyc_smoke import SMOKE_IMPORTS + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +def test_pyproject_registers_sybase_and_sqlanywhere_extras_entrypoints_and_markers() -> None: + """pyproject.toml registers optional dependencies, sqlglot dialect entry points, and pytest markers.""" + config = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text()) + + optional_deps = config["project"]["optional-dependencies"] + assert optional_deps.get("sybase") == ["pymssql"] + assert optional_deps.get("sqlanywhere") == ["sqlanydb>=1.0.14"] + + dialect_entrypoints = config["project"]["entry-points"]["sqlglot.dialects"] + assert dialect_entrypoints.get("sybase") == "sqlspec.dialects.sybase:Sybase" + assert dialect_entrypoints.get("sqlanywhere") == "sqlspec.dialects.sybase:SQLAnywhere" + + markers = config["tool"]["pytest"]["ini_options"]["markers"] + assert any(marker.startswith("sybase:") for marker in markers) + assert any(marker.startswith("sqlanywhere:") for marker in markers) + + mypy_overrides = config["tool"]["mypy"]["overrides"] + ignored_modules: set[str] = set() + for override in mypy_overrides: + if override.get("ignore_missing_imports"): + modules = override.get("module", []) + if isinstance(modules, list): + ignored_modules.update(modules) + assert "sqlanydb" in ignored_modules + assert "sqlanydb.*" in ignored_modules + + +def test_mypyc_inventory_includes_sybase_and_sqlanywhere_hot_surfaces() -> None: + """mypyc_inventory validates cleanly and classifies Sybase and SQL Anywhere modules accurately.""" + errors = validate_inventory(PROJECT_ROOT) + assert errors == [] + + inventory = build_inventory(PROJECT_ROOT) + compiled = set(inventory["compiled_modules"]) + interpreted = set(inventory["interpreted_modules"]) + hot_surfaces = inventory["hot_surfaces"] + + expected_compiled = { + "sqlspec/dialects/sybase/_generators.py", + "sqlspec/dialects/sybase/_parsers.py", + "sqlspec/dialects/sybase/_transforms.py", + "sqlspec/data_dictionary/dialects/sybase/config.py", + "sqlspec/data_dictionary/dialects/sqlanywhere/config.py", + "sqlspec/adapters/sybase/core.py", + "sqlspec/adapters/sybase/pool.py", + "sqlspec/adapters/sqlanywhere/core.py", + "sqlspec/adapters/sqlanywhere/pool.py", + } + for module_path in expected_compiled: + assert module_path in compiled, f"Expected compiled module: {module_path}" + assert module_path in hot_surfaces, f"Expected hot_surfaces entry: {module_path}" + assert hot_surfaces[module_path]["status"] == "compiled" + + expected_interpreted = { + "sqlspec/dialects/sybase/__init__.py", + "sqlspec/adapters/sybase/_typing.py", + "sqlspec/adapters/sybase/config.py", + "sqlspec/adapters/sybase/data_dictionary.py", + "sqlspec/adapters/sybase/driver.py", + "sqlspec/adapters/sqlanywhere/_typing.py", + "sqlspec/adapters/sqlanywhere/config.py", + "sqlspec/adapters/sqlanywhere/data_dictionary.py", + "sqlspec/adapters/sqlanywhere/driver.py", + } + for module_path in expected_interpreted: + assert module_path in interpreted, f"Expected interpreted module: {module_path}" + + +def test_mypyc_smoke_imports_include_sybase_and_sqlanywhere() -> None: + """SMOKE_IMPORTS includes dialect and compiled helper/pool entries for Sybase and SQL Anywhere.""" + smoke_by_name = {entry.name: entry for entry in SMOKE_IMPORTS} + assert "sybase_dialect" in smoke_by_name + assert "sqlanywhere_dialect" in smoke_by_name + assert "sybase_generators" in smoke_by_name + assert smoke_by_name["sybase_generators"].require_compiled is True + assert "sybase_pool" in smoke_by_name + assert smoke_by_name["sybase_pool"].require_compiled is True + assert "sqlanywhere_pool" in smoke_by_name + assert smoke_by_name["sqlanywhere_pool"].require_compiled is True + + +def test_importing_sybase_and_sqlanywhere_adapters_does_not_eagerly_import_driver_packages() -> None: + """Importing sqlspec.adapters.sybase and sqlspec.adapters.sqlanywhere defers pymssql and sqlanydb.""" + script = ( + "import sys\n" + "import sqlspec.adapters.sybase\n" + "import sqlspec.adapters.sqlanywhere\n" + "leaked = [name for name in ('pymssql', 'sqlanydb', 'rich') if name in sys.modules]\n" + "assert not leaked, f'Eagerly imported: {leaked}'\n" + ) + subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, check=True) diff --git a/tools/scripts/mypyc_inventory.py b/tools/scripts/mypyc_inventory.py index b0c4aed56..0efd5cc60 100644 --- a/tools/scripts/mypyc_inventory.py +++ b/tools/scripts/mypyc_inventory.py @@ -111,6 +111,14 @@ "classification": "compile_now", "reason": "Spanner data-dictionary dialect configuration is in the compiled dialect surface.", }, + "sqlspec/data_dictionary/dialects/sqlanywhere/config.py": { + "classification": "compile_now", + "reason": "SQL Anywhere data-dictionary dialect configuration and feature helpers compile with the dialect surface.", + }, + "sqlspec/data_dictionary/dialects/sybase/config.py": { + "classification": "compile_now", + "reason": "Sybase ASE data-dictionary dialect configuration and feature helpers compile with the dialect surface.", + }, "sqlspec/data_dictionary/dialects/bigquery/config.py": { "classification": "compile_now", "reason": "Shared BigQuery INFORMATION_SCHEMA formatting helpers for native BigQuery and ADBC-as-BigQuery dictionaries.", @@ -161,6 +169,18 @@ }, "sqlspec/dialects/db2/_parsers.py": {"classification": "compile_now", "reason": "Db2 AST normalisation helpers."}, "sqlspec/dialects/db2/_transforms.py": {"classification": "compile_now", "reason": "Db2 render helper functions."}, + "sqlspec/dialects/sybase/_generators.py": { + "classification": "compile_now", + "reason": "Sybase and SQL Anywhere render handlers compile as explicit-argument callables with per-instance generator dispatch.", + }, + "sqlspec/dialects/sybase/_parsers.py": { + "classification": "compile_now", + "reason": "Sybase and SQL Anywhere AST normalisation helpers.", + }, + "sqlspec/dialects/sybase/_transforms.py": { + "classification": "compile_now", + "reason": "Sybase and SQL Anywhere render helper functions.", + }, "sqlspec/adapters/mysql_common.py": { "classification": "compile_now", "reason": "Shared MySQL-family adapter helpers compile with the adapter core surface.", @@ -251,6 +271,22 @@ "classification": "compile_now", "reason": "Pool runtime compiles without adapter driver exception-handler subclasses.", }, + "sqlspec/adapters/sqlanywhere/core.py": { + "classification": "compile_now", + "reason": "SQL Anywhere parameter normalization, row collection, and exception mapping helpers compile with the adapter core surface.", + }, + "sqlspec/adapters/sqlanywhere/pool.py": { + "classification": "compile_now", + "reason": "SQL Anywhere thread-local pool runtime compiles without adapter driver subclasses.", + }, + "sqlspec/adapters/sybase/core.py": { + "classification": "compile_now", + "reason": "Sybase ASE parameter normalization, percent escaping, row collection, and exception mapping helpers compile with the adapter core surface.", + }, + "sqlspec/adapters/sybase/pool.py": { + "classification": "compile_now", + "reason": "Sybase ASE thread-local pool runtime with TDSVER='5.0' negotiation compiles without adapter driver subclasses.", + }, "sqlspec/adapters/aiosqlite/pool.py": { "classification": "compile_now", "reason": "Async pool runtime compiles without adapter driver exception-handler subclasses.", diff --git a/tools/scripts/mypyc_smoke.py b/tools/scripts/mypyc_smoke.py index c53fe936a..81f15174f 100644 --- a/tools/scripts/mypyc_smoke.py +++ b/tools/scripts/mypyc_smoke.py @@ -44,11 +44,16 @@ class SmokeImport(NamedTuple): SmokeImport("storage_backend_fsspec", "sqlspec.storage.backends.fsspec", "FSSpecBackend", True), SmokeImport("storage_backend_obstore", "sqlspec.storage.backends.obstore", "ObStoreBackend", True), SmokeImport("sqlite_pool", "sqlspec.adapters.sqlite.pool", "SqliteConnectionPool", True), + SmokeImport("sybase_pool", "sqlspec.adapters.sybase.pool", "SybaseConnectionPool", True), + SmokeImport("sqlanywhere_pool", "sqlspec.adapters.sqlanywhere.pool", "SqlanywhereConnectionPool", True), SmokeImport("data_dictionary_registry", "sqlspec.data_dictionary._registry", "get_dialect_config", True), SmokeImport("data_dictionary_loader", "sqlspec.data_dictionary._loader", "DataDictionaryLoader", True), SmokeImport("pgvector_dialect", "sqlspec.dialects.postgres._pgvector", "PGVector"), SmokeImport("spanner_dialect", "sqlspec.dialects.spanner._spanner", "Spanner"), SmokeImport("db2_dialect", "sqlspec.dialects.db2", "DB2"), + SmokeImport("sybase_dialect", "sqlspec.dialects.sybase", "Sybase"), + SmokeImport("sqlanywhere_dialect", "sqlspec.dialects.sybase", "SQLAnywhere"), + SmokeImport("sybase_generators", "sqlspec.dialects.sybase._generators", "sybase_select_sql", True), SmokeImport("fastapi_providers", "sqlspec.extensions.fastapi.providers", "provide_filters", True, "fastapi"), SmokeImport( "litestar_providers", "sqlspec.extensions.litestar.providers", "create_filter_dependencies", True, "litestar" @@ -587,6 +592,7 @@ def _discover_adapter_config_classes(*, skipped: "list[str] | None" = None) -> " "psycopg_pool", "pymssql", "pymysql", + "sqlanydb", }: raise skipped.append(f"{module_name}: optional dependency missing: {exc.name}") From 061c022779b46cd8714ce8eea906b642f70024c7 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:17:42 +0000 Subject: [PATCH 29/35] feat(data-dictionary,utils): align sybase and sqlanywhere fixtures and migrations - add sybase and sqlanywhere sequence and identity resync support to fixtures - support replaces argument in sybase and sqlanywhere migration trackers - exclude lob types from export order by clause --- sqlspec/adapters/sqlanywhere/migrations.py | 21 ++- sqlspec/adapters/sybase/migrations.py | 21 ++- sqlspec/utils/fixtures.py | 8 +- tests/unit/utils/test_fixtures.py | 137 ++++++++++++++++- tests/unit/utils/test_fixtures_sybase.py | 167 --------------------- 5 files changed, 175 insertions(+), 179 deletions(-) delete mode 100644 tests/unit/utils/test_fixtures_sybase.py diff --git a/sqlspec/adapters/sqlanywhere/migrations.py b/sqlspec/adapters/sqlanywhere/migrations.py index c85c3b501..1d5e11d45 100644 --- a/sqlspec/adapters/sqlanywhere/migrations.py +++ b/sqlspec/adapters/sqlanywhere/migrations.py @@ -109,23 +109,36 @@ def record_migration( checksum: str, *, commit: bool = True, + replaces: "Sequence[str] | None" = None, ) -> None: """Record a successfully applied migration with SQL Anywhere-compatible metadata.""" parsed_version = parse_version(version) version_type = parsed_version.type.value result = driver.execute(self._next_execution_sequence_query()) next_sequence = result.get_data()[0]["next_seq"] if result.data else 1 - driver.execute( - self._record_migration_statement( + applied_by = os.environ.get("USER", "unknown") + if replaces: + statement = self._record_squashed_migration_statement( version, version_type, next_sequence, description, execution_time_ms, checksum, - os.environ.get("USER", "unknown"), + applied_by, + ",".join(replaces), ) - ) + else: + statement = self._record_migration_statement( + version, + version_type, + next_sequence, + description, + execution_time_ms, + checksum, + applied_by, + ) + driver.execute(statement) if commit: self._commit_if_needed(driver) diff --git a/sqlspec/adapters/sybase/migrations.py b/sqlspec/adapters/sybase/migrations.py index e6b8d3f2b..c3e41c17b 100644 --- a/sqlspec/adapters/sybase/migrations.py +++ b/sqlspec/adapters/sybase/migrations.py @@ -107,23 +107,36 @@ def record_migration( checksum: str, *, commit: bool = True, + replaces: "Sequence[str] | None" = None, ) -> None: """Record a successfully applied migration with Sybase ASE-compatible metadata.""" parsed_version = parse_version(version) version_type = parsed_version.type.value result = driver.execute(self._next_execution_sequence_query()) next_sequence = result.get_data()[0]["next_seq"] if result.data else 1 - driver.execute( - self._record_migration_statement( + applied_by = os.environ.get("USER", "unknown") + if replaces: + statement = self._record_squashed_migration_statement( version, version_type, next_sequence, description, execution_time_ms, checksum, - os.environ.get("USER", "unknown"), + applied_by, + ",".join(replaces), ) - ) + else: + statement = self._record_migration_statement( + version, + version_type, + next_sequence, + description, + execution_time_ms, + checksum, + applied_by, + ) + driver.execute(statement) if commit: self._commit_if_needed(driver) diff --git a/sqlspec/utils/fixtures.py b/sqlspec/utils/fixtures.py index 5774fa84f..4706f2a42 100644 --- a/sqlspec/utils/fixtures.py +++ b/sqlspec/utils/fixtures.py @@ -8,6 +8,7 @@ import array import base64 import gzip +import importlib import os import re import secrets @@ -19,7 +20,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Final, NamedTuple -from sqlglot import exp +from sqlglot import Dialect, exp from sqlspec import dialects as _dialects from sqlspec.core.parameters import TypedParameter @@ -78,7 +79,9 @@ "oracle", "postgres", "postgresql", + "sqlanywhere", "sqlite", + "sybase", "tsql", }) _MYSQL_DIALECTS: Final["frozenset[str]"] = frozenset({"mariadb", "mysql"}) @@ -679,6 +682,8 @@ def _sqlglot_dialect(dialect: "DialectType", family: str) -> "DialectType": """ if family in {"postgres", "mysql", "tsql"}: return family + if family in {"sybase", "sqlanywhere"} and family not in Dialect.classes: + importlib.import_module("sqlspec.dialects.sybase") return dialect @@ -1835,7 +1840,6 @@ def _table_export_query(dialect: "DialectType", family: str, table: str, table_c order_columns = [column.name for column in table_columns if column.is_primary] or [ column.name for column in table_columns if _is_orderable_type(family, column.data_type) ] - ] order_by: list[exp.Expr] = [exp.Column(this=_quoted_identifier(name)) for name in order_columns] if not order_by and not table_columns: order_by = [exp.Literal.number(1)] diff --git a/tests/unit/utils/test_fixtures.py b/tests/unit/utils/test_fixtures.py index e7b82f569..abe680eb6 100644 --- a/tests/unit/utils/test_fixtures.py +++ b/tests/unit/utils/test_fixtures.py @@ -15,11 +15,11 @@ import struct import sys import zipfile -from datetime import datetime, time, timedelta, timezone +from datetime import UTC, datetime, time, timedelta, timezone from decimal import Decimal from pathlib import Path from typing import Any, cast -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, call, patch from uuid import UUID import pytest @@ -2748,3 +2748,136 @@ def test_duckdb_sequence_resync_increment_and_schema(tmp_path: Path) -> None: assert empty_next_id == 1 finally: config.close_pool() + + +def test_sybase_identity_resync_executes_sp_chgattribute_sync(tmp_path: Path) -> None: + """Sybase fixture load with resync_sequences=True should execute sp_chgattribute when max > 0.""" + rows = [{"id": 42, "name": "Alice"}] + (tmp_path / "users.json").write_text(json.dumps(rows), encoding="utf-8") + driver = MagicMock() + driver.statement_config.dialect = "sybase" + driver.data_dictionary.get_columns.return_value = [ + {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, + {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + ] + driver.select_value.return_value = 42 + + counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) + + assert counts == {"users": 1} + assert driver.execute_many.call_args.args[1] == [{"id": 42, "name": "Alice"}] + driver.select_value.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') + assert call("EXEC sp_chgattribute 'users', 'identity_burn_max', 0, '42'") in driver.execute.call_args_list + + +def test_sybase_identity_resync_skips_when_max_is_zero(tmp_path: Path) -> None: + """Sybase fixture load should skip sp_chgattribute when table max is 0.""" + (tmp_path / "users.json").write_text("[]", encoding="utf-8") + driver = MagicMock() + driver.statement_config.dialect = "sybase" + driver.data_dictionary.get_columns.return_value = [ + {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, + {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + ] + driver.select_value.return_value = 0 + + counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) + + assert counts == {"users": 0} + driver.select_value.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') + driver.execute.assert_not_called() + + +async def test_sqlanywhere_identity_resync_executes_sa_reset_identity_async(tmp_path: Path) -> None: + """SQL Anywhere async fixture load should execute CALL sa_reset_identity with table, schema, and max.""" + rows = [{"id": 15, "name": "Bob"}] + (tmp_path / "DBA.users.json").write_text(json.dumps(rows), encoding="utf-8") + driver = MagicMock() + driver.statement_config.dialect = "sqlanywhere" + driver.data_dictionary.get_columns = AsyncMock( + return_value=[ + {"column_name": "id", "data_type": "integer", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, + {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + ] + ) + driver.execute_many = AsyncMock() + driver.execute = AsyncMock() + driver.select_value = AsyncMock(return_value=15) + + counts = await load_table_fixtures_async(driver, tmp_path, resync_sequences=True) + + assert counts == {"DBA.users": 1} + assert driver.execute_many.call_args.args[1] == [{"id": 15, "name": "Bob"}] + driver.select_value.assert_awaited_once_with('SELECT coalesce(max("id"), 0) FROM "DBA"."users"') + driver.execute.assert_awaited_once_with("CALL sa_reset_identity('users', 'DBA', 15)") + + +def test_sybase_and_sqlanywhere_lob_types_excluded_from_export_order_by(tmp_path: Path) -> None: + """Unorderable LOB types in Sybase and SQL Anywhere should be excluded from export ORDER BY.""" + driver = MagicMock() + driver.statement_config.dialect = "sybase" + driver.data_dictionary.get_columns.return_value = [ + {"column_name": "code", "data_type": "varchar", "is_primary": False}, + {"column_name": "notes_text", "data_type": "text", "is_primary": False}, + {"column_name": "notes_unitext", "data_type": "unitext", "is_primary": False}, + {"column_name": "photo", "data_type": "image", "is_primary": False}, + {"column_name": "sa_clob", "data_type": "long varchar", "is_primary": False}, + {"column_name": "sa_nclob", "data_type": "long nvarchar", "is_primary": False}, + {"column_name": "sa_blob", "data_type": "long binary", "is_primary": False}, + ] + driver.select.return_value = [] + + export_table_fixtures_sync(driver, tmp_path, ["docs"], compress=False) + + select_query = driver.select.call_args.args[0] + order_by_clause = select_query.partition("ORDER BY")[2] + assert "[code]" in order_by_clause or '"code"' in order_by_clause + for lob_col in ("notes_text", "notes_unitext", "photo", "sa_clob", "sa_nclob", "sa_blob"): + assert lob_col not in order_by_clause + + +def test_sybase_and_sqlanywhere_value_decoders(tmp_path: Path) -> None: + """Sybase and SQL Anywhere temporal, monetary, UUID, and binary column types should decode on load.""" + rows = [ + { + "id": 1, + "big_dt": "2026-10-30T12:34:56.123456+00:00", + "small_dt": "2026-10-30T12:34:00+00:00", + "dt_tz": "2026-10-30T12:34:56+00:00", + "big_t": "12:34:56.123456", + "price": "19.95", + "small_price": "5.50", + "ext_id": "7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f", + "raw_image": "AP8=", + "raw_long_bin": "AP8=", + } + ] + (tmp_path / "records.json").write_text(json.dumps(rows), encoding="utf-8") + columns = [ + {"column_name": "id", "data_type": "int", "is_primary": True}, + {"column_name": "big_dt", "data_type": "bigdatetime", "is_primary": False}, + {"column_name": "small_dt", "data_type": "smalldatetime", "is_primary": False}, + {"column_name": "dt_tz", "data_type": "datetimetz", "is_primary": False}, + {"column_name": "big_t", "data_type": "bigtime", "is_primary": False}, + {"column_name": "price", "data_type": "money", "is_primary": False}, + {"column_name": "small_price", "data_type": "smallmoney", "is_primary": False}, + {"column_name": "ext_id", "data_type": "uniqueidentifier", "is_primary": False}, + {"column_name": "raw_image", "data_type": "image", "is_primary": False}, + {"column_name": "raw_long_bin", "data_type": "long binary", "is_primary": False}, + ] + driver = MagicMock() + driver.statement_config.dialect = "sybase" + driver.data_dictionary.get_columns.return_value = columns + + load_table_fixtures_sync(driver, tmp_path) + + loaded_row = driver.execute_many.call_args.args[1][0] + assert loaded_row["big_dt"] == datetime(2026, 10, 30, 12, 34, 56, 123456, tzinfo=UTC) + assert loaded_row["small_dt"] == datetime(2026, 10, 30, 12, 34, 0, tzinfo=UTC) + assert loaded_row["dt_tz"] == datetime(2026, 10, 30, 12, 34, 56, tzinfo=UTC) + assert loaded_row["big_t"] == time(12, 34, 56, 123456) + assert loaded_row["price"] == Decimal("19.95") + assert loaded_row["small_price"] == Decimal("5.50") + assert loaded_row["ext_id"] == UUID("7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f") + assert loaded_row["raw_image"] == b"\x00\xff" + assert loaded_row["raw_long_bin"] == b"\x00\xff" diff --git a/tests/unit/utils/test_fixtures_sybase.py b/tests/unit/utils/test_fixtures_sybase.py deleted file mode 100644 index 35a5e6723..000000000 --- a/tests/unit/utils/test_fixtures_sybase.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Unit tests for Sybase ASE and SAP SQL Anywhere fixture loading, decoding, and identity resync.""" - -import json -from datetime import UTC, datetime, time -from decimal import Decimal -from pathlib import Path -from typing import Any -from unittest.mock import AsyncMock, MagicMock -from uuid import UUID - -from sqlspec.utils.fixtures import export_table_fixtures_sync, load_table_fixtures_async, load_table_fixtures_sync - - -def _mock_sync_driver(dialect: str, columns: list[dict[str, Any]], max_identity: int = 42) -> MagicMock: - driver = MagicMock() - driver.statement_config.dialect = dialect - driver.data_dictionary.get_columns.return_value = columns - driver.select_value_or_none.return_value = max_identity - return driver - - -def _mock_async_driver(dialect: str, columns: list[dict[str, Any]], max_identity: int = 42) -> MagicMock: - driver = MagicMock() - driver.statement_config.dialect = dialect - driver.data_dictionary.get_columns = AsyncMock(return_value=columns) - driver.execute_many = AsyncMock() - driver.execute = AsyncMock() - driver.select_value_or_none = AsyncMock(return_value=max_identity) - return driver - - -SYBASE_IDENTITY_COLUMNS: list[dict[str, Any]] = [ - {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "YES", "is_generated": 0}, - { - "column_name": "name", - "data_type": "varchar", - "is_primary": False, - "identity_generation": None, - "is_generated": 0, - }, -] - -SQLANYWHERE_IDENTITY_COLUMNS: list[dict[str, Any]] = [ - { - "column_name": "id", - "data_type": "integer", - "is_primary": True, - "identity_generation": "AUTOINCREMENT", - "is_generated": 0, - }, - { - "column_name": "name", - "data_type": "varchar", - "is_primary": False, - "identity_generation": None, - "is_generated": 0, - }, -] - - -def test_sybase_identity_resync_executes_sp_chgattribute_sync(tmp_path: Path) -> None: - """Sybase fixture load with resync_sequences=True should execute sp_chgattribute when max > 0.""" - rows = [{"id": 42, "name": "Alice"}] - (tmp_path / "users.json").write_text(json.dumps(rows), encoding="utf-8") - driver = _mock_sync_driver("sybase", SYBASE_IDENTITY_COLUMNS, max_identity=42) - - counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) - - assert counts == {"users": 1} - assert driver.execute_many.call_args.args[1] == [{"id": 42, "name": "Alice"}] - driver.select_value_or_none.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') - driver.execute.assert_called_once_with("EXEC sp_chgattribute 'users', 'identity_burn_max', 0, '42'") - - -def test_sybase_identity_resync_skips_when_max_is_zero(tmp_path: Path) -> None: - """Sybase fixture load should skip sp_chgattribute when table max is 0.""" - (tmp_path / "users.json").write_text("[]", encoding="utf-8") - driver = _mock_sync_driver("sybase", SYBASE_IDENTITY_COLUMNS, max_identity=0) - - counts = load_table_fixtures_sync(driver, tmp_path, resync_sequences=True) - - assert counts == {"users": 0} - driver.select_value_or_none.assert_called_once_with('SELECT coalesce(max("id"), 0) FROM "users"') - driver.execute.assert_not_called() - - -async def test_sqlanywhere_identity_resync_executes_sa_reset_identity_async(tmp_path: Path) -> None: - """SQL Anywhere async fixture load should execute CALL sa_reset_identity with table, schema, and max.""" - rows = [{"id": 15, "name": "Bob"}] - (tmp_path / "DBA.users.json").write_text(json.dumps(rows), encoding="utf-8") - driver = _mock_async_driver("sqlanywhere", SQLANYWHERE_IDENTITY_COLUMNS, max_identity=15) - - counts = await load_table_fixtures_async(driver, tmp_path, resync_sequences=True) - - assert counts == {"DBA.users": 1} - assert driver.execute_many.call_args.args[1] == [{"id": 15, "name": "Bob"}] - driver.select_value_or_none.assert_awaited_once_with('SELECT coalesce(max("id"), 0) FROM "DBA"."users"') - driver.execute.assert_awaited_once_with("CALL sa_reset_identity('users', 'DBA', 15)") - - -def test_sybase_and_sqlanywhere_lob_types_excluded_from_export_order_by(tmp_path: Path) -> None: - """Unorderable LOB types in Sybase and SQL Anywhere should be excluded from export ORDER BY.""" - driver = MagicMock() - driver.statement_config.dialect = "sybase" - driver.data_dictionary.get_columns.return_value = [ - {"column_name": "code", "data_type": "varchar", "is_primary": False}, - {"column_name": "notes_text", "data_type": "text", "is_primary": False}, - {"column_name": "notes_unitext", "data_type": "unitext", "is_primary": False}, - {"column_name": "photo", "data_type": "image", "is_primary": False}, - {"column_name": "sa_clob", "data_type": "long varchar", "is_primary": False}, - {"column_name": "sa_nclob", "data_type": "long nvarchar", "is_primary": False}, - {"column_name": "sa_blob", "data_type": "long binary", "is_primary": False}, - ] - driver.select.return_value = [] - - export_table_fixtures_sync(driver, tmp_path, ["docs"], compress=False) - - select_query = driver.select.call_args.args[0] - order_by_clause = select_query.to_statement().sql.partition("ORDER BY")[2] - assert "[code]" in order_by_clause or '"code"' in order_by_clause - for lob_col in ("notes_text", "notes_unitext", "photo", "sa_clob", "sa_nclob", "sa_blob"): - assert lob_col not in order_by_clause - - -def test_sybase_and_sqlanywhere_value_decoders(tmp_path: Path) -> None: - """Sybase and SQL Anywhere temporal, monetary, UUID, and binary column types should decode on load.""" - rows = [ - { - "id": 1, - "big_dt": "2026-10-30T12:34:56.123456+00:00", - "small_dt": "2026-10-30T12:34:00+00:00", - "dt_tz": "2026-10-30T12:34:56+00:00", - "big_t": "12:34:56.123456", - "price": "19.95", - "small_price": "5.50", - "ext_id": "7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f", - "raw_image": "AP8=", - "raw_long_bin": "AP8=", - } - ] - (tmp_path / "records.json").write_text(json.dumps(rows), encoding="utf-8") - columns = [ - {"column_name": "id", "data_type": "int", "is_primary": True}, - {"column_name": "big_dt", "data_type": "bigdatetime", "is_primary": False}, - {"column_name": "small_dt", "data_type": "smalldatetime", "is_primary": False}, - {"column_name": "dt_tz", "data_type": "datetimetz", "is_primary": False}, - {"column_name": "big_t", "data_type": "bigtime", "is_primary": False}, - {"column_name": "price", "data_type": "money", "is_primary": False}, - {"column_name": "small_price", "data_type": "smallmoney", "is_primary": False}, - {"column_name": "ext_id", "data_type": "uniqueidentifier", "is_primary": False}, - {"column_name": "raw_image", "data_type": "image", "is_primary": False}, - {"column_name": "raw_long_bin", "data_type": "long binary", "is_primary": False}, - ] - driver = _mock_sync_driver("sybase", columns) - - load_table_fixtures_sync(driver, tmp_path) - - loaded_row = driver.execute_many.call_args.args[1][0] - assert loaded_row["big_dt"] == datetime(2026, 10, 30, 12, 34, 56, 123456, tzinfo=UTC) - assert loaded_row["small_dt"] == datetime(2026, 10, 30, 12, 34, 0, tzinfo=UTC) - assert loaded_row["dt_tz"] == datetime(2026, 10, 30, 12, 34, 56, tzinfo=UTC) - assert loaded_row["big_t"] == time(12, 34, 56, 123456) - assert loaded_row["price"] == Decimal("19.95") - assert loaded_row["small_price"] == Decimal("5.50") - assert loaded_row["ext_id"] == UUID("7f3c1d2e-9a4b-4c5d-8e6f-0a1b2c3d4e5f") - assert loaded_row["raw_image"] == b"\x00\xff" - assert loaded_row["raw_long_bin"] == b"\x00\xff" From d256a0e1487871c470f9806d3e7c649466c52156 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:25:17 +0000 Subject: [PATCH 30/35] fix(dialects): correct sybase and sqlanywhere pagination, timestamps, and types --- sqlspec/dialects/sybase/__init__.py | 17 ++++--- sqlspec/dialects/sybase/_generators.py | 68 ++++++++++++++++--------- sqlspec/dialects/sybase/_parsers.py | 32 +++++++++--- sqlspec/dialects/sybase/_transforms.py | 21 ++++++-- tests/unit/dialects/test_sqlanywhere.py | 27 ++++++++++ tests/unit/dialects/test_sybase.py | 10 +++- 6 files changed, 132 insertions(+), 43 deletions(-) diff --git a/sqlspec/dialects/sybase/__init__.py b/sqlspec/dialects/sybase/__init__.py index 264f45a38..f3c5c83a5 100644 --- a/sqlspec/dialects/sybase/__init__.py +++ b/sqlspec/dialects/sybase/__init__.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any from sqlglot import TokenType, exp, generator -from sqlglot.dialects.dialect import Dialect, NormalizationStrategy +from sqlglot.dialects.dialect import NormalizationStrategy from sqlglot.dialects.tsql import TSQL from sqlspec.dialects.sybase._generators import sqlanywhere_dispatch, sybase_dispatch @@ -73,7 +73,16 @@ def parse_into(self, expression_type: Any, sql: str, **opts: Any) -> "list[exp.E class SQLAnywhereTokenizer(TSQL.Tokenizer): """Tokenizer for SAP SQL Anywhere SQL syntax.""" - KEYWORDS = {**TSQL.Tokenizer.KEYWORDS, "BLOB": TokenType.BLOB, "UNIQUEIDENTIFIER": TokenType.UUID} + KEYWORDS = { + **TSQL.Tokenizer.KEYWORDS, + "BLOB": TokenType.BLOB, + "CURRENT DATE": TokenType.CURRENT_DATE, + "CURRENT TIME": TokenType.CURRENT_TIME, + "CURRENT TIMESTAMP": TokenType.CURRENT_TIMESTAMP, + "CURRENT USER": TokenType.CURRENT_USER, + "TIMESTAMP": TokenType.TIMESTAMP, + "UNIQUEIDENTIFIER": TokenType.UUID, + } def tokenize(self, sql: str) -> "list[Token]": """Tokenize SAP SQL Anywhere SQL and normalize SQL Anywhere token sequences.""" @@ -104,7 +113,3 @@ def parse_into(self, expression_type: Any, sql: str, **opts: Any) -> "list[exp.E """Parse SAP SQL Anywhere SQL into a target expression type.""" parsed = self.parser(**opts).parse_into(expression_type, self.tokenize(sql), sql) return [normalize_sqlanywhere_expression(expr) if expr is not None else None for expr in parsed] - - -Dialect.classes["sybase"] = Sybase -Dialect.classes["sqlanywhere"] = SQLAnywhere diff --git a/sqlspec/dialects/sybase/_generators.py b/sqlspec/dialects/sybase/_generators.py index abad841f9..0363faad6 100644 --- a/sqlspec/dialects/sybase/_generators.py +++ b/sqlspec/dialects/sybase/_generators.py @@ -20,14 +20,14 @@ "SYBASE_DEFAULT_TYPE_LENGTHS", "SYBASE_TRANSFORMS", "SYBASE_TYPE_MAPPING", - "char_length_sql", - "charindex_sql", - "ilike_sql", + "sqlanywhere_anonymous_sql", + "sqlanywhere_autoincrement_sql", "sqlanywhere_create_sql", "sqlanywhere_current_timestamp_sql", "sqlanywhere_datatype_sql", "sqlanywhere_dispatch", "sqlanywhere_drop_sql", + "sqlanywhere_generated_identity_sql", "sqlanywhere_select_sql", "sqlanywhere_set_operation_sql", "sybase_autoincrement_sql", @@ -61,8 +61,9 @@ exp.DType.TIMESTAMPTZ: "BIGDATETIME", exp.DType.TIMESTAMPLTZ: "BIGDATETIME", exp.DType.TIMESTAMPNTZ: "BIGDATETIME", - exp.DType.DATETIME: "BIGDATETIME", + exp.DType.DATETIME: "DATETIME", exp.DType.DATETIME2: "BIGDATETIME", + exp.DType.ROWVERSION: "TIMESTAMP", exp.DType.DATE: "DATE", exp.DType.TIME: "BIGTIME", exp.DType.TIMETZ: "BIGTIME", @@ -184,9 +185,12 @@ def sybase_autoincrement_sql(gen: "generator.Generator", expression: exp.AutoInc def sybase_current_timestamp_sql(gen: "generator.Generator", expression: exp.CurrentTimestamp) -> str: - """Render CurrentTimestamp as GETUTCDATE().""" - _ = (gen, expression) - return "GETUTCDATE()" + """Render CurrentTimestamp as GETDATE() or GETUTCDATE() when UTC is specified.""" + _ = gen + zone = expression.args.get("zone") + if isinstance(expression, exp.UtcTimestamp) or (zone is not None and "utc" in str(zone).lower()): + return "GETUTCDATE()" + return "GETDATE()" def sybase_returning_sql(gen: "generator.Generator", expression: exp.Returning) -> str: @@ -240,24 +244,35 @@ def sqlanywhere_drop_sql(gen: "generator.Generator", expression: exp.Drop) -> st def sqlanywhere_current_timestamp_sql(gen: "generator.Generator", expression: exp.CurrentTimestamp) -> str: - """Render CurrentTimestamp as CURRENT UTC TIMESTAMP.""" - _ = (gen, expression) - return "CURRENT UTC TIMESTAMP" + """Render CurrentTimestamp as CURRENT TIMESTAMP or CURRENT UTC TIMESTAMP when UTC is specified.""" + _ = gen + zone = expression.args.get("zone") + if isinstance(expression, exp.UtcTimestamp) or (zone is not None and "utc" in str(zone).lower()): + return "CURRENT UTC TIMESTAMP" + return "CURRENT TIMESTAMP" -def char_length_sql(gen: "generator.Generator", expression: exp.Length) -> str: - """Render exp.Length as CHAR_LENGTH(this).""" - return render_char_length(gen, expression) +def sqlanywhere_anonymous_sql(gen: "generator.Generator", expression: exp.Anonymous) -> str: + """Render Anonymous functions, mapping GETUTCDATE to CURRENT UTC TIMESTAMP.""" + if str(expression.this).upper() == "GETUTCDATE": + return "CURRENT UTC TIMESTAMP" + return generator.Generator.anonymous_sql(gen, expression) -def charindex_sql(gen: "generator.Generator", expression: exp.StrPosition) -> str: - """Render exp.StrPosition as CHARINDEX(substr, this[, position]).""" - return render_charindex(gen, expression) +def sqlanywhere_generated_identity_sql( + gen: "generator.Generator", expression: exp.GeneratedAsIdentityColumnConstraint +) -> str: + """Render GeneratedAsIdentityColumnConstraint as DEFAULT AUTOINCREMENT.""" + _ = (gen, expression) + return "DEFAULT AUTOINCREMENT" -def ilike_sql(gen: "generator.Generator", expression: exp.ILike) -> str: - """Render exp.ILike as LOWER(this) LIKE LOWER(expression).""" - return render_ilike(gen, expression) +def sqlanywhere_autoincrement_sql( + gen: "generator.Generator", expression: exp.AutoIncrementColumnConstraint +) -> str: + """Render AutoIncrementColumnConstraint as DEFAULT AUTOINCREMENT.""" + _ = (gen, expression) + return "DEFAULT AUTOINCREMENT" SYBASE_TRANSFORMS: Final[dict[type[exp.Expr], Callable[[Any, Any], str]]] = { @@ -271,12 +286,13 @@ def ilike_sql(gen: "generator.Generator", expression: exp.ILike) -> str: exp.AutoIncrementColumnConstraint: sybase_autoincrement_sql, exp.CurrentTimestamp: sybase_current_timestamp_sql, exp.Returning: sybase_returning_sql, - exp.Length: char_length_sql, - exp.StrPosition: charindex_sql, - exp.ILike: ilike_sql, + exp.Length: render_char_length, + exp.StrPosition: render_charindex, + exp.ILike: render_ilike, } SQLANYWHERE_TRANSFORMS: Final[dict[type[exp.Expr], Callable[[Any, Any], str]]] = { + exp.Anonymous: sqlanywhere_anonymous_sql, exp.Select: sqlanywhere_select_sql, exp.Union: sqlanywhere_set_operation_sql, exp.Intersect: sqlanywhere_set_operation_sql, @@ -284,10 +300,12 @@ def ilike_sql(gen: "generator.Generator", expression: exp.ILike) -> str: exp.DataType: sqlanywhere_datatype_sql, exp.Create: sqlanywhere_create_sql, exp.Drop: sqlanywhere_drop_sql, + exp.GeneratedAsIdentityColumnConstraint: sqlanywhere_generated_identity_sql, + exp.AutoIncrementColumnConstraint: sqlanywhere_autoincrement_sql, exp.CurrentTimestamp: sqlanywhere_current_timestamp_sql, - exp.Length: char_length_sql, - exp.StrPosition: charindex_sql, - exp.ILike: ilike_sql, + exp.Length: render_char_length, + exp.StrPosition: render_charindex, + exp.ILike: render_ilike, } _sybase_overlay_cache: "tuple[dict[type[exp.Expr], Callable[..., str]], dict[type[exp.Expr], Callable[..., str]]] | None" = None diff --git a/sqlspec/dialects/sybase/_parsers.py b/sqlspec/dialects/sybase/_parsers.py index 4ad5347dc..461874d0d 100644 --- a/sqlspec/dialects/sybase/_parsers.py +++ b/sqlspec/dialects/sybase/_parsers.py @@ -69,7 +69,7 @@ def normalize_sqlanywhere_tokens(tokens: "list[Token]") -> "list[Token]": result: list[Token] = [] statement: list[Token] = [] for token in rewritten: - if token.token_type == TokenType.SEMICOLON: + if token.token_type in {TokenType.SEMICOLON, TokenType.UNION, TokenType.INTERSECT, TokenType.EXCEPT}: result.extend(_rewrite_top_start_at_statement(statement)) result.append(token) statement = [] @@ -172,19 +172,35 @@ def _rewrite_top_start_at_statement(statement: "list[Token]") -> "list[Token]": return statement if start_val_tok.token_type == TokenType.NUMBER and start_val_tok.text.isdigit(): offset_str = str(max(int(start_val_tok.text) - 1, 0)) - offset_tok = Token( - TokenType.NUMBER, offset_str, start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end - ) + offset_tokens = [ + Token( + TokenType.NUMBER, offset_str, start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end + ) + ] else: - offset_tok = start_val_tok + offset_tokens = [ + Token(TokenType.L_PAREN, "(", start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.start), + start_val_tok, + Token(TokenType.DASH, "-", start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end), + Token(TokenType.NUMBER, "1", start_val_tok.line, start_val_tok.col, start_val_tok.end, start_val_tok.end), + Token(TokenType.R_PAREN, ")", start_val_tok.line, start_val_tok.col, start_val_tok.end, start_val_tok.end), + ] + body = statement[header_index + _TOP_START_AT_SPAN :] last = statement[-1] tail = [ Token(TokenType.LIMIT, "LIMIT", last.line, last.col, last.end, last.end), limit_tok, Token(TokenType.OFFSET, "OFFSET", last.line, last.col, last.end, last.end), - offset_tok, + *offset_tokens, ] - return [*statement[:header_index], *statement[header_index + _TOP_START_AT_SPAN :], *tail] + insert_idx = len(body) + for idx in range(len(body) - 1, -1, -1): + if body[idx].token_type == TokenType.FOR and idx + 1 < len(body): + next_text = body[idx + 1].text.upper() + if next_text in {"UPDATE", "READ"}: + insert_idx = idx + break + return [*statement[:header_index], *body[:insert_idx], *tail, *body[insert_idx:]] def _canonical_sybase_node(node: exp.Expr) -> exp.Expr: @@ -197,4 +213,6 @@ def _canonical_sybase_node(node: exp.Expr) -> exp.Expr: this = node.expressions[1] position = node.expressions[2] if len(node.expressions) == _CHARINDEX_MAX_ARGS else None return exp.StrPosition(this=this, substr=substr, position=position) + if func_name in {"GETUTCDATE", "CURRENT UTC TIMESTAMP"}: + return exp.UtcTimestamp() return node diff --git a/sqlspec/dialects/sybase/_transforms.py b/sqlspec/dialects/sybase/_transforms.py index 0b09f2532..8efef9fe9 100644 --- a/sqlspec/dialects/sybase/_transforms.py +++ b/sqlspec/dialects/sybase/_transforms.py @@ -14,7 +14,10 @@ def is_literal_integer_limit(limit_expr: Any) -> bool: - """Return True when limit_expr is a literal integer exp.Limit without parameters.""" + """Return True when limit_expr is a literal integer exp.Limit or exp.Fetch without parameters.""" + if isinstance(limit_expr, exp.Fetch): + count_expr = limit_expr.args.get("count") + return isinstance(count_expr, exp.Literal) and not count_expr.is_string if not isinstance(limit_expr, exp.Limit): return False count_expr = limit_expr.expression @@ -22,15 +25,25 @@ def is_literal_integer_limit(limit_expr: Any) -> bool: def clone_with_detached_pagination(expression: exp.Expr) -> "tuple[exp.Expr, exp.Limit | None, exp.Offset | None]": - """Return a shallow copy of expression with limit and offset removed if present.""" + """Return a shallow copy of expression with limit, offset, and fetch removed if present.""" limit_arg = expression.args.get("limit") offset_arg = expression.args.get("offset") - if limit_arg is None and offset_arg is None: + fetch_arg = expression.args.get("fetch") + if limit_arg is None and offset_arg is None and fetch_arg is None: return expression, None, None cloned = expression.copy() cloned.set("limit", None) cloned.set("offset", None) - limit_node = limit_arg if isinstance(limit_arg, exp.Limit) else None + cloned.set("fetch", None) + limit_node: exp.Limit | None = None + if isinstance(limit_arg, exp.Limit): + limit_node = limit_arg + elif isinstance(limit_arg, exp.Fetch): + count_expr = limit_arg.args.get("count") + limit_node = exp.Limit(expression=count_expr.copy() if count_expr is not None else None) + elif isinstance(fetch_arg, exp.Fetch): + count_expr = fetch_arg.args.get("count") + limit_node = exp.Limit(expression=count_expr.copy() if count_expr is not None else None) offset_node = offset_arg if isinstance(offset_arg, exp.Offset) else None return cloned, limit_node, offset_node diff --git a/tests/unit/dialects/test_sqlanywhere.py b/tests/unit/dialects/test_sqlanywhere.py index 2d509a93f..9c586794d 100644 --- a/tests/unit/dialects/test_sqlanywhere.py +++ b/tests/unit/dialects/test_sqlanywhere.py @@ -89,3 +89,30 @@ def test_sqlanywhere_long_types_and_utc_timestamp_parsing() -> None: assert "LONG VARCHAR" in rendered assert "LONG BINARY" in rendered assert "UNIQUEIDENTIFIER" in rendered + + +def test_sqlanywhere_parse_top_start_at_non_literal() -> None: + """Verify Watcom-SQL SELECT TOP n START AT ? parses with offset decrement.""" + parsed = parse_one("SELECT TOP 10 START AT ? id, name FROM users ORDER BY id", dialect="sqlanywhere") + assert parsed.sql(dialect="sqlanywhere") == "SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET (? - 1)" + + +def test_sqlanywhere_current_timestamp_local_and_utc() -> None: + """Verify CURRENT_TIMESTAMP renders local and GETUTCDATE renders UTC.""" + assert ( + transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sqlanywhere")[0] == "SELECT CURRENT TIMESTAMP" + ) + assert transpile("SELECT GETUTCDATE()", read="tsql", write="sqlanywhere")[0] == "SELECT CURRENT UTC TIMESTAMP" + + +def test_sqlanywhere_identity_column_rendering() -> None: + """Verify generated identity and autoincrement constraints render DEFAULT AUTOINCREMENT.""" + create_sql = parse_one("CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY, name VARCHAR(50))").sql("sqlanywhere") + assert "DEFAULT AUTOINCREMENT" in create_sql + + +def test_sqlanywhere_fetch_pagination_preservation() -> None: + """Verify FETCH FIRST n ROWS ONLY and OFFSET m ROWS FETCH NEXT n are preserved and not dropped.""" + fetch_expr = parse_one("SELECT id FROM users ORDER BY id OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY", read="tsql") + rendered = fetch_expr.sql("sqlanywhere") + assert rendered == "SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 5" diff --git a/tests/unit/dialects/test_sybase.py b/tests/unit/dialects/test_sybase.py index eb21eeac3..f185f0347 100644 --- a/tests/unit/dialects/test_sybase.py +++ b/tests/unit/dialects/test_sybase.py @@ -116,7 +116,8 @@ def test_sybase_functions_and_operators() -> None: assert transpile("SELECT STRPOS(name, 'abc') FROM users", read="postgres", write="sybase")[0] == ( "SELECT CHARINDEX('abc', name) FROM users" ) - assert transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sybase")[0] == "SELECT GETUTCDATE()" + assert transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sybase")[0] == "SELECT GETDATE()" + assert transpile("SELECT GETUTCDATE()", read="tsql", write="sybase")[0] == "SELECT GETUTCDATE()" assert transpile("SELECT * FROM users WHERE name ILIKE '%alice%'", read="postgres", write="sybase")[0] == ( "SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%alice%')" ) @@ -127,3 +128,10 @@ def test_sybase_functions_and_operators() -> None: pos_node = parsed_pos.find(exp.StrPosition) assert pos_node is not None assert pos_node.sql(dialect="sybase") == "CHARINDEX('abc', name)" + + +def test_sybase_fetch_pagination_preservation() -> None: + """Verify FETCH FIRST n ROWS ONLY and OFFSET m ROWS FETCH NEXT n are preserved and not dropped.""" + fetch_expr = parse_one("SELECT id FROM users ORDER BY id OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY", read="tsql") + rendered = fetch_expr.sql("sybase") + assert rendered == "SELECT id FROM users ORDER BY id ROWS LIMIT 10 OFFSET 5" From 8cb14a26d064f8b3a163fc94638d72b6f6e1f26a Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:29:26 +0000 Subject: [PATCH 31/35] feat(data-dictionary): rewrite sybase and sqlanywhere catalog queries for canonical column metadata --- .../dialects/sqlanywhere/sql/columns.sql | 80 ++++++----- .../dialects/sqlanywhere/sql/tables.sql | 61 ++++++--- .../dialects/sybase/sql/columns.sql | 126 ++++++++++-------- .../dialects/sybase/sql/constraints.sql | 52 +++++++- .../dialects/sybase/sql/foreign_keys.sql | 50 +++++-- .../dialects/sybase/sql/indexes.sql | 26 +++- .../dialects/sybase/sql/tables.sql | 22 +-- 7 files changed, 278 insertions(+), 139 deletions(-) diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql index 09be767eb..6c8602cc1 100644 --- a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/columns.sql @@ -1,5 +1,21 @@ -- name: by_schema -- dialect: sqlanywhere +WITH pk_cols AS ( + SELECT DISTINCT + i.table_id, + ic.column_id + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i.index_category = 1 +), +unique_cols AS ( + SELECT DISTINCT + i.table_id, + ic.column_id + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i."unique" IN (1, 2) +) SELECT u.user_name AS schema_name, t.table_name AS table_name, @@ -12,28 +28,20 @@ SELECT c.width AS max_length, c.width AS numeric_precision, c.scale AS numeric_scale, - CASE WHEN c.pkey = 'Y' THEN 1 ELSE 0 END AS is_primary, - CASE - WHEN EXISTS ( - SELECT 1 - FROM SYS.SYSIDX i - JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id - WHERE i.table_id = t.table_id - AND i."unique" IN (1, 2) - AND ic.column_id = c.column_id - ) THEN 1 - ELSE 0 - END AS is_unique, - CASE - WHEN c."default" LIKE '%autoincrement%' THEN 'AUTOINCREMENT' - ELSE NULL - END AS identity_generation, + CASE WHEN pk.column_id IS NOT NULL THEN 1 ELSE 0 END AS is_primary, + CASE WHEN uq.column_id IS NOT NULL THEN 1 ELSE 0 END AS is_unique, + CASE WHEN c."default" LIKE '%autoincrement%' THEN 1 ELSE 0 END AS is_identity, + CASE WHEN c."default" LIKE '%autoincrement%' THEN 'ALWAYS' ELSE NULL END AS identity_generation, CASE WHEN c.column_type = 'C' THEN 1 ELSE 0 END AS is_generated, + CASE WHEN c.column_type = 'C' THEN c."default" ELSE NULL END AS generation_expression, + 0 AS is_hidden, NULL AS column_comment FROM SYS.SYSTABCOL c JOIN SYS.SYSTAB t ON c.table_id = t.table_id JOIN SYS.SYSUSER u ON t.creator = u.user_id JOIN SYS.SYSDOMAIN d ON c.domain_id = d.domain_id +LEFT JOIN pk_cols pk ON pk.table_id = t.table_id AND pk.column_id = c.column_id +LEFT JOIN unique_cols uq ON uq.table_id = t.table_id AND uq.column_id = c.column_id WHERE t.table_type IN (1, 21) AND u.user_name = COALESCE(:schema_name, CURRENT USER) AND (:table_name IS NULL OR t.table_name = :table_name) @@ -41,6 +49,22 @@ ORDER BY u.user_name, t.table_name, c.column_id; -- name: by_table -- dialect: sqlanywhere +WITH pk_cols AS ( + SELECT DISTINCT + i.table_id, + ic.column_id + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i.index_category = 1 +), +unique_cols AS ( + SELECT DISTINCT + i.table_id, + ic.column_id + FROM SYS.SYSIDX i + JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id + WHERE i."unique" IN (1, 2) +) SELECT u.user_name AS schema_name, t.table_name AS table_name, @@ -53,28 +77,20 @@ SELECT c.width AS max_length, c.width AS numeric_precision, c.scale AS numeric_scale, - CASE WHEN c.pkey = 'Y' THEN 1 ELSE 0 END AS is_primary, - CASE - WHEN EXISTS ( - SELECT 1 - FROM SYS.SYSIDX i - JOIN SYS.SYSIDXCOL ic ON i.table_id = ic.table_id AND i.index_id = ic.index_id - WHERE i.table_id = t.table_id - AND i."unique" IN (1, 2) - AND ic.column_id = c.column_id - ) THEN 1 - ELSE 0 - END AS is_unique, - CASE - WHEN c."default" LIKE '%autoincrement%' THEN 'AUTOINCREMENT' - ELSE NULL - END AS identity_generation, + CASE WHEN pk.column_id IS NOT NULL THEN 1 ELSE 0 END AS is_primary, + CASE WHEN uq.column_id IS NOT NULL THEN 1 ELSE 0 END AS is_unique, + CASE WHEN c."default" LIKE '%autoincrement%' THEN 1 ELSE 0 END AS is_identity, + CASE WHEN c."default" LIKE '%autoincrement%' THEN 'ALWAYS' ELSE NULL END AS identity_generation, CASE WHEN c.column_type = 'C' THEN 1 ELSE 0 END AS is_generated, + CASE WHEN c.column_type = 'C' THEN c."default" ELSE NULL END AS generation_expression, + 0 AS is_hidden, NULL AS column_comment FROM SYS.SYSTABCOL c JOIN SYS.SYSTAB t ON c.table_id = t.table_id JOIN SYS.SYSUSER u ON t.creator = u.user_id JOIN SYS.SYSDOMAIN d ON c.domain_id = d.domain_id +LEFT JOIN pk_cols pk ON pk.table_id = t.table_id AND pk.column_id = c.column_id +LEFT JOIN unique_cols uq ON uq.table_id = t.table_id AND uq.column_id = c.column_id WHERE t.table_type IN (1, 21) AND t.table_name = :table_name AND u.user_name = COALESCE(:schema_name, CURRENT USER) diff --git a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql index 993eda662..c683d6bd0 100644 --- a/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql +++ b/sqlspec/data_dictionary/dialects/sqlanywhere/sql/tables.sql @@ -1,29 +1,54 @@ -- name: by_schema -- dialect: sqlanywhere +WITH RECURSIVE dependency_tree (table_id, schema_name, table_name, dependency_level) AS ( + SELECT + t.table_id, + u.user_name AS schema_name, + t.table_name AS table_name, + 0 AS dependency_level + FROM SYS.SYSTAB t + JOIN SYS.SYSUSER u ON t.creator = u.user_id + WHERE t.table_type = 1 + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR t.table_name = :table_name) + AND NOT EXISTS ( + SELECT 1 + FROM SYS.SYSFKEY fk + WHERE fk.foreign_table_id = t.table_id + AND fk.primary_table_id <> t.table_id + ) + UNION ALL + SELECT + child.table_id, + u.user_name AS schema_name, + child.table_name AS table_name, + parent.dependency_level + 1 AS dependency_level + FROM SYS.SYSFKEY fk + JOIN SYS.SYSTAB child ON child.table_id = fk.foreign_table_id + JOIN SYS.SYSUSER u ON child.creator = u.user_id + JOIN dependency_tree parent ON parent.table_id = fk.primary_table_id + WHERE child.table_type = 1 + AND fk.primary_table_id <> fk.foreign_table_id + AND u.user_name = COALESCE(:schema_name, CURRENT USER) + AND (:table_name IS NULL OR child.table_name = :table_name) + AND parent.dependency_level < 20 +), +dependency_levels AS ( + SELECT + table_id, + MAX(dependency_level) AS dependency_level + FROM dependency_tree + GROUP BY table_id +) SELECT u.user_name AS schema_name, t.table_name AS table_name, 'BASE TABLE' AS table_type, - CASE - WHEN EXISTS ( - SELECT 1 - FROM SYS.SYSFKEY fk - WHERE fk.foreign_table_id = t.table_id - AND fk.primary_table_id <> t.table_id - ) THEN 1 - ELSE 0 - END AS dependency_level, - CASE - WHEN EXISTS ( - SELECT 1 - FROM SYS.SYSFKEY fk - WHERE fk.foreign_table_id = t.table_id - AND fk.primary_table_id <> t.table_id - ) THEN 1 - ELSE 0 - END AS level + COALESCE(dl.dependency_level, 0) AS dependency_level, + COALESCE(dl.dependency_level, 0) AS level FROM SYS.SYSTAB t JOIN SYS.SYSUSER u ON t.creator = u.user_id +LEFT JOIN dependency_levels dl ON dl.table_id = t.table_id WHERE t.table_type = 1 AND u.user_name = COALESCE(:schema_name, CURRENT USER) AND (:table_name IS NULL OR t.table_name = :table_name) diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql b/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql index fead85118..574116a4a 100644 --- a/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql +++ b/sqlspec/data_dictionary/dialects/sybase/sql/columns.sql @@ -12,40 +12,49 @@ SELECT c.length AS max_length, c.prec AS numeric_precision, c.scale AS numeric_scale, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysindexes i - WHERE i.id = o.id - AND (i.status & 2048) = 2048 - AND ( - index_col(u.name + '.' + o.name, i.indid, 1) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 2) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 3) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 4) = c.name - ) - ) THEN 1 - ELSE 0 - END AS is_primary, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysindexes i - WHERE i.id = o.id - AND (i.status & 2) = 2 - AND i.keycnt = 1 - AND index_col(u.name + '.' + o.name, i.indid, 1) = c.name - ) THEN 1 - ELSE 0 - END AS is_unique, - CASE WHEN (c.status & 128) = 128 THEN 'YES' ELSE NULL END AS identity_generation, + CASE WHEN pk.col_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary, + CASE WHEN uq.col_name IS NOT NULL THEN 1 ELSE 0 END AS is_unique, + CASE WHEN (c.status & 128) = 128 THEN 1 ELSE 0 END AS is_identity, + CASE WHEN (c.status & 128) = 128 THEN 'ALWAYS' ELSE NULL END AS identity_generation, 0 AS is_generated, + NULL AS generation_expression, + 0 AS is_hidden, NULL AS column_comment FROM dbo.syscolumns c JOIN dbo.sysobjects o ON c.id = o.id JOIN dbo.sysusers u ON o.uid = u.uid JOIN dbo.systypes t ON c.usertype = t.usertype LEFT JOIN dbo.syscomments cm ON c.cdefault = cm.id AND cm.colid = 1 +LEFT JOIN ( + SELECT DISTINCT + i.id, + index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) AS col_name + FROM dbo.sysindexes i + JOIN dbo.sysobjects o ON i.id = o.id + CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 + ) k + WHERE (i.status & 2048) = 2048 + AND index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) IS NOT NULL +) pk ON pk.id = o.id AND pk.col_name = c.name +LEFT JOIN ( + SELECT DISTINCT + i.id, + index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) AS col_name + FROM dbo.sysindexes i + JOIN dbo.sysobjects o ON i.id = o.id + CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 + ) k + WHERE (i.status & 2) = 2 + AND index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) IS NOT NULL +) uq ON uq.id = o.id AND uq.col_name = c.name WHERE o.type IN ('U', 'V') AND u.name = COALESCE(:schema_name, USER_NAME()) AND (:table_name IS NULL OR o.name = :table_name) @@ -65,40 +74,49 @@ SELECT c.length AS max_length, c.prec AS numeric_precision, c.scale AS numeric_scale, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysindexes i - WHERE i.id = o.id - AND (i.status & 2048) = 2048 - AND ( - index_col(u.name + '.' + o.name, i.indid, 1) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 2) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 3) = c.name - OR index_col(u.name + '.' + o.name, i.indid, 4) = c.name - ) - ) THEN 1 - ELSE 0 - END AS is_primary, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysindexes i - WHERE i.id = o.id - AND (i.status & 2) = 2 - AND i.keycnt = 1 - AND index_col(u.name + '.' + o.name, i.indid, 1) = c.name - ) THEN 1 - ELSE 0 - END AS is_unique, - CASE WHEN (c.status & 128) = 128 THEN 'YES' ELSE NULL END AS identity_generation, + CASE WHEN pk.col_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary, + CASE WHEN uq.col_name IS NOT NULL THEN 1 ELSE 0 END AS is_unique, + CASE WHEN (c.status & 128) = 128 THEN 1 ELSE 0 END AS is_identity, + CASE WHEN (c.status & 128) = 128 THEN 'ALWAYS' ELSE NULL END AS identity_generation, 0 AS is_generated, + NULL AS generation_expression, + 0 AS is_hidden, NULL AS column_comment FROM dbo.syscolumns c JOIN dbo.sysobjects o ON c.id = o.id JOIN dbo.sysusers u ON o.uid = u.uid JOIN dbo.systypes t ON c.usertype = t.usertype LEFT JOIN dbo.syscomments cm ON c.cdefault = cm.id AND cm.colid = 1 +LEFT JOIN ( + SELECT DISTINCT + i.id, + index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) AS col_name + FROM dbo.sysindexes i + JOIN dbo.sysobjects o ON i.id = o.id + CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 + ) k + WHERE (i.status & 2048) = 2048 + AND index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) IS NOT NULL +) pk ON pk.id = o.id AND pk.col_name = c.name +LEFT JOIN ( + SELECT DISTINCT + i.id, + index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) AS col_name + FROM dbo.sysindexes i + JOIN dbo.sysobjects o ON i.id = o.id + CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 + ) k + WHERE (i.status & 2) = 2 + AND index_col(USER_NAME(o.uid) + '.' + o.name, i.indid, k.pos) IS NOT NULL +) uq ON uq.id = o.id AND uq.col_name = c.name WHERE o.type IN ('U', 'V') AND o.name = :table_name AND u.name = COALESCE(:schema_name, USER_NAME()) diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql b/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql index 377c96e13..5354e5412 100644 --- a/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql +++ b/sqlspec/data_dictionary/dialects/sybase/sql/constraints.sql @@ -20,7 +20,31 @@ JOIN dbo.sysobjects co ON c.constrid = co.id LEFT JOIN dbo.syscomments cm ON co.id = cm.id AND cm.colid = 1 WHERE u.name = COALESCE(:schema_name, USER_NAME()) AND (:table_name IS NULL OR o.name = :table_name) -ORDER BY u.name, o.name, co.name; +UNION +SELECT + u.name AS schema_name, + o.name AS table_name, + i.name AS constraint_name, + CASE + WHEN (i.status & 2048) = 2048 THEN 'PRIMARY KEY' + WHEN (i.status & 2) = 2 THEN 'UNIQUE' + END AS constraint_type, + NULL AS check_clause, + 'Y' AS is_enforced +FROM dbo.sysindexes i +JOIN dbo.sysobjects o ON i.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND ((i.status & 2048) = 2048 OR (i.status & 2) = 2) + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND (:table_name IS NULL OR o.name = :table_name) + AND NOT EXISTS ( + SELECT 1 + FROM dbo.sysconstraints sc + JOIN dbo.sysobjects sco ON sc.constrid = sco.id + WHERE sc.tableid = o.id AND sco.name = i.name + ) +ORDER BY schema_name, table_name, constraint_name; -- name: by_table -- dialect: sybase @@ -44,4 +68,28 @@ JOIN dbo.sysobjects co ON c.constrid = co.id LEFT JOIN dbo.syscomments cm ON co.id = cm.id AND cm.colid = 1 WHERE o.name = :table_name AND u.name = COALESCE(:schema_name, USER_NAME()) -ORDER BY co.name; +UNION +SELECT + u.name AS schema_name, + o.name AS table_name, + i.name AS constraint_name, + CASE + WHEN (i.status & 2048) = 2048 THEN 'PRIMARY KEY' + WHEN (i.status & 2) = 2 THEN 'UNIQUE' + END AS constraint_type, + NULL AS check_clause, + 'Y' AS is_enforced +FROM dbo.sysindexes i +JOIN dbo.sysobjects o ON i.id = o.id +JOIN dbo.sysusers u ON o.uid = u.uid +WHERE o.type = 'U' + AND ((i.status & 2048) = 2048 OR (i.status & 2) = 2) + AND o.name = :table_name + AND u.name = COALESCE(:schema_name, USER_NAME()) + AND NOT EXISTS ( + SELECT 1 + FROM dbo.sysconstraints sc + JOIN dbo.sysobjects sco ON sc.constrid = sco.id + WHERE sc.tableid = o.id AND sco.name = i.name + ) +ORDER BY constraint_name; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql b/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql index 72e1a9e93..82d483c08 100644 --- a/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql +++ b/sqlspec/data_dictionary/dialects/sybase/sql/foreign_keys.sql @@ -14,11 +14,28 @@ JOIN dbo.sysusers u ON o.uid = u.uid JOIN dbo.sysobjects co ON r.constrid = co.id JOIN dbo.sysobjects ro ON r.reftabid = ro.id JOIN dbo.sysusers ru ON ro.uid = ru.uid -JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = r.fokey1 -JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = r.refkey1 -WHERE u.name = COALESCE(:schema_name, USER_NAME()) +CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 +) k +JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = CASE k.pos + WHEN 1 THEN r.fokey1 WHEN 2 THEN r.fokey2 WHEN 3 THEN r.fokey3 WHEN 4 THEN r.fokey4 + WHEN 5 THEN r.fokey5 WHEN 6 THEN r.fokey6 WHEN 7 THEN r.fokey7 WHEN 8 THEN r.fokey8 + WHEN 9 THEN r.fokey9 WHEN 10 THEN r.fokey10 WHEN 11 THEN r.fokey11 WHEN 12 THEN r.fokey12 + WHEN 13 THEN r.fokey13 WHEN 14 THEN r.fokey14 WHEN 15 THEN r.fokey15 WHEN 16 THEN r.fokey16 +END +JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = CASE k.pos + WHEN 1 THEN r.refkey1 WHEN 2 THEN r.refkey2 WHEN 3 THEN r.refkey3 WHEN 4 THEN r.refkey4 + WHEN 5 THEN r.refkey5 WHEN 6 THEN r.refkey6 WHEN 7 THEN r.refkey7 WHEN 8 THEN r.refkey8 + WHEN 9 THEN r.refkey9 WHEN 10 THEN r.refkey10 WHEN 11 THEN r.refkey11 WHEN 12 THEN r.refkey12 + WHEN 13 THEN r.refkey13 WHEN 14 THEN r.refkey14 WHEN 15 THEN r.refkey15 WHEN 16 THEN r.refkey16 +END +WHERE k.pos <= r.frgcnt + AND u.name = COALESCE(:schema_name, USER_NAME()) AND (:table_name IS NULL OR o.name = :table_name) -ORDER BY u.name, o.name, co.name; +ORDER BY u.name, o.name, co.name, k.pos; -- name: by_table -- dialect: sybase @@ -36,8 +53,25 @@ JOIN dbo.sysusers u ON o.uid = u.uid JOIN dbo.sysobjects co ON r.constrid = co.id JOIN dbo.sysobjects ro ON r.reftabid = ro.id JOIN dbo.sysusers ru ON ro.uid = ru.uid -JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = r.fokey1 -JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = r.refkey1 -WHERE o.name = :table_name +CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 +) k +JOIN dbo.syscolumns fc ON fc.id = o.id AND fc.colid = CASE k.pos + WHEN 1 THEN r.fokey1 WHEN 2 THEN r.fokey2 WHEN 3 THEN r.fokey3 WHEN 4 THEN r.fokey4 + WHEN 5 THEN r.fokey5 WHEN 6 THEN r.fokey6 WHEN 7 THEN r.fokey7 WHEN 8 THEN r.fokey8 + WHEN 9 THEN r.fokey9 WHEN 10 THEN r.fokey10 WHEN 11 THEN r.fokey11 WHEN 12 THEN r.fokey12 + WHEN 13 THEN r.fokey13 WHEN 14 THEN r.fokey14 WHEN 15 THEN r.fokey15 WHEN 16 THEN r.fokey16 +END +JOIN dbo.syscolumns rc ON rc.id = ro.id AND rc.colid = CASE k.pos + WHEN 1 THEN r.refkey1 WHEN 2 THEN r.refkey2 WHEN 3 THEN r.refkey3 WHEN 4 THEN r.refkey4 + WHEN 5 THEN r.refkey5 WHEN 6 THEN r.refkey6 WHEN 7 THEN r.refkey7 WHEN 8 THEN r.refkey8 + WHEN 9 THEN r.refkey9 WHEN 10 THEN r.refkey10 WHEN 11 THEN r.refkey11 WHEN 12 THEN r.refkey12 + WHEN 13 THEN r.refkey13 WHEN 14 THEN r.refkey14 WHEN 15 THEN r.refkey15 WHEN 16 THEN r.refkey16 +END +WHERE k.pos <= r.frgcnt + AND o.name = :table_name AND u.name = COALESCE(:schema_name, USER_NAME()) -ORDER BY co.name; +ORDER BY co.name, k.pos; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql b/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql index 58c15fd02..c78a3853c 100644 --- a/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql +++ b/sqlspec/data_dictionary/dialects/sybase/sql/indexes.sql @@ -4,18 +4,25 @@ SELECT u.name AS schema_name, o.name AS table_name, i.name AS index_name, - index_col(u.name + '.' + o.name, i.indid, 1) AS column_name, - 1 AS column_position, + index_col(u.name + '.' + o.name, i.indid, k.pos) AS column_name, + k.pos AS column_position, CASE WHEN (i.status & 2) = 2 THEN 1 ELSE 0 END AS is_unique, CASE WHEN (i.status & 2048) = 2048 THEN 1 ELSE 0 END AS is_primary FROM dbo.sysindexes i JOIN dbo.sysobjects o ON i.id = o.id JOIN dbo.sysusers u ON o.uid = u.uid +CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 +) k WHERE o.type = 'U' AND i.indid BETWEEN 1 AND 254 + AND index_col(u.name + '.' + o.name, i.indid, k.pos) IS NOT NULL AND u.name = COALESCE(:schema_name, USER_NAME()) AND (:table_name IS NULL OR o.name = :table_name) -ORDER BY u.name, o.name, i.name; +ORDER BY u.name, o.name, i.name, k.pos; -- name: by_table -- dialect: sybase @@ -23,15 +30,22 @@ SELECT u.name AS schema_name, o.name AS table_name, i.name AS index_name, - index_col(u.name + '.' + o.name, i.indid, 1) AS column_name, - 1 AS column_position, + index_col(u.name + '.' + o.name, i.indid, k.pos) AS column_name, + k.pos AS column_position, CASE WHEN (i.status & 2) = 2 THEN 1 ELSE 0 END AS is_unique, CASE WHEN (i.status & 2048) = 2048 THEN 1 ELSE 0 END AS is_primary FROM dbo.sysindexes i JOIN dbo.sysobjects o ON i.id = o.id JOIN dbo.sysusers u ON o.uid = u.uid +CROSS JOIN ( + SELECT 1 AS pos UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 + UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 + UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 + UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 +) k WHERE o.type = 'U' AND i.indid BETWEEN 1 AND 254 + AND index_col(u.name + '.' + o.name, i.indid, k.pos) IS NOT NULL AND o.name = :table_name AND u.name = COALESCE(:schema_name, USER_NAME()) -ORDER BY i.name; +ORDER BY i.name, k.pos; diff --git a/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql b/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql index a91957f04..0df8acb6d 100644 --- a/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql +++ b/sqlspec/data_dictionary/dialects/sybase/sql/tables.sql @@ -4,30 +4,14 @@ SELECT u.name AS schema_name, o.name AS table_name, 'BASE TABLE' AS table_type, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysreferences r - WHERE r.tableid = o.id - AND r.reftabid <> o.id - ) THEN 1 - ELSE 0 - END AS dependency_level, - CASE - WHEN EXISTS ( - SELECT 1 - FROM dbo.sysreferences r - WHERE r.tableid = o.id - AND r.reftabid <> o.id - ) THEN 1 - ELSE 0 - END AS level + 0 AS dependency_level, + 0 AS level FROM dbo.sysobjects o JOIN dbo.sysusers u ON o.uid = u.uid WHERE o.type = 'U' AND u.name = COALESCE(:schema_name, USER_NAME()) AND (:table_name IS NULL OR o.name = :table_name) -ORDER BY dependency_level, u.name, o.name; +ORDER BY u.name, o.name; -- name: all_by_schema -- dialect: sybase From 738d0c26e6e1da4db2e5979dc629af50f27c12dc Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:38:45 +0000 Subject: [PATCH 32/35] fix(adapters): harden sybase and sqlanywhere stores, session rollback, and driver configs --- sqlspec/adapters/sqlanywhere/_typing.py | 15 ++++++- sqlspec/adapters/sqlanywhere/adk/store.py | 6 +-- sqlspec/adapters/sqlanywhere/core.py | 3 +- .../adapters/sqlanywhere/data_dictionary.py | 6 ++- sqlspec/adapters/sqlanywhere/driver.py | 5 +++ sqlspec/adapters/sqlanywhere/migrations.py | 3 ++ sqlspec/adapters/sybase/_typing.py | 7 +++- sqlspec/adapters/sybase/adk/store.py | 9 +++-- sqlspec/adapters/sybase/config.py | 2 + sqlspec/adapters/sybase/data_dictionary.py | 4 ++ sqlspec/adapters/sybase/events/store.py | 28 +++++++++++++ sqlspec/adapters/sybase/migrations.py | 3 ++ sqlspec/adapters/sybase/pool.py | 33 +++++++-------- tests/unit/adapters/test_sqlanywhere.py | 2 +- tests/unit/adapters/test_sybase.py | 40 ++++++++++--------- 15 files changed, 119 insertions(+), 47 deletions(-) diff --git a/sqlspec/adapters/sqlanywhere/_typing.py b/sqlspec/adapters/sqlanywhere/_typing.py index 03d4d6450..8c2b61e2e 100644 --- a/sqlspec/adapters/sqlanywhere/_typing.py +++ b/sqlspec/adapters/sqlanywhere/_typing.py @@ -14,6 +14,11 @@ SQLANYDB_MODULE: Any = None _DRIVER_CLS: Any = None + +class _SqlanywhereUnavailableError(Exception): + """Fallback SQL Anywhere exception base when sqlanydb is not installed.""" + + if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType @@ -31,7 +36,7 @@ SqlanywhereConnection = Any SqlanywhereRawCursor = Any SqlanywhereQueryParams = Any - SqlanywhereError = Exception + SqlanywhereError = import_optional_attr("sqlanydb", "Error") or _SqlanywhereUnavailableError __all__ = ( "SQLANYDB_MODULE", @@ -108,7 +113,13 @@ def __enter__(self) -> "SqlanywhereDriver": def __exit__( self, exc_type: "type[BaseException] | None", exc_val: "BaseException | None", exc_tb: "TracebackType | None" ) -> "bool | None": - if self._connection is not None: + if self._connection is None: + return None + try: + if self._driver is not None: + self._driver._release_open_work() + finally: self._release_connection(self._connection) self._connection = None + self._driver = None return None diff --git a/sqlspec/adapters/sqlanywhere/adk/store.py b/sqlspec/adapters/sqlanywhere/adk/store.py index 90f00548e..6b94477ae 100644 --- a/sqlspec/adapters/sqlanywhere/adk/store.py +++ b/sqlspec/adapters/sqlanywhere/adk/store.py @@ -506,7 +506,7 @@ def search_entries( FROM {_table_ref(self._memory_table)} WHERE {where_scope} AND content_text LIKE ? ESCAPE '\\' ORDER BY timestamp DESC - ROWS LIMIT ? + LIMIT ? """ rows = self._execute_fetchall(sql, (*scope_params, f"%{escaped_query}%", limit_value)) return [_memory_record_from_row(row) for row in rows] @@ -717,7 +717,7 @@ def _events_query( params.append(after_timestamp) limit_clause = "" if limit is not None: - limit_clause = "\n ROWS LIMIT ?" + limit_clause = "\n LIMIT ?" params.append(limit) sql = f""" SELECT id, app_name, user_id, session_id, invocation_id, timestamp, event_data @@ -864,7 +864,7 @@ def _session_list_query( page_clause = "" if limit is not None: params.extend((limit, offset)) - page_clause = "\n ROWS LIMIT ? OFFSET ?" + page_clause = "\n LIMIT ? OFFSET ?" sql = f""" SELECT id, app_name, user_id, state, create_time, update_time diff --git a/sqlspec/adapters/sqlanywhere/core.py b/sqlspec/adapters/sqlanywhere/core.py index 7a8a690e1..f36e7240d 100644 --- a/sqlspec/adapters/sqlanywhere/core.py +++ b/sqlspec/adapters/sqlanywhere/core.py @@ -334,7 +334,8 @@ def extract_sqlstate_and_sqlcode(error: BaseException) -> tuple[str | None, int sqlstate: str | None = getattr(error, "sqlstate", None) sqlcode: int | None = getattr(error, "errorcode", getattr(error, "sqlcode", None)) - args: tuple[Any, ...] = error.args if isinstance(getattr(error, "args", None), tuple) else () + raw_args = getattr(error, "args", None) + args: list[Any] = list(raw_args) if isinstance(raw_args, tuple) else [] if len(args) >= _SQLANYDB_FULL_ARGS_LEN: if sqlstate is None and isinstance(args[0], str): sqlstate = args[0] diff --git a/sqlspec/adapters/sqlanywhere/data_dictionary.py b/sqlspec/adapters/sqlanywhere/data_dictionary.py index f9fe77395..dce3659cb 100644 --- a/sqlspec/adapters/sqlanywhere/data_dictionary.py +++ b/sqlspec/adapters/sqlanywhere/data_dictionary.py @@ -73,13 +73,17 @@ def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetad column_default=str(r["column_default"]) if r.get("column_default") is not None else None, ordinal_position=int(r.get("ordinal_position", 1)), max_length=int(r.get("max_length", 0)) if r.get("max_length") is not None else 0, + numeric_precision=int(r.get("numeric_precision", 0)) if r.get("numeric_precision") is not None else 0, numeric_scale=int(r.get("numeric_scale", 0)) if r.get("numeric_scale") is not None else 0, is_primary=bool(r.get("is_primary")), is_unique=bool(r.get("is_unique")), + is_identity=bool(r.get("is_identity")), identity_generation=str(r["identity_generation"]) if r.get("identity_generation") - else ("BY DEFAULT" if r.get("is_identity") else None), + else ("ALWAYS" if r.get("is_identity") else None), is_generated=bool(r.get("is_generated")), + generation_expression=str(r["generation_expression"]) if r.get("generation_expression") is not None else None, + is_hidden=bool(r.get("is_hidden")), ) for r in rows ] diff --git a/sqlspec/adapters/sqlanywhere/driver.py b/sqlspec/adapters/sqlanywhere/driver.py index 0d3cc60ac..908f800c3 100644 --- a/sqlspec/adapters/sqlanywhere/driver.py +++ b/sqlspec/adapters/sqlanywhere/driver.py @@ -227,6 +227,11 @@ def rollback(self) -> None: finally: self._transaction_active = False + def _release_open_work(self) -> None: + """Roll back work left open before connection is returned to the pool.""" + with contextlib.suppress(SQLSpecError): + self.rollback() + def with_cursor(self, connection: "SqlanywhereConnection") -> "SqlanywhereCursor": return SqlanywhereCursor(connection) diff --git a/sqlspec/adapters/sqlanywhere/migrations.py b/sqlspec/adapters/sqlanywhere/migrations.py index 1d5e11d45..8ff433580 100644 --- a/sqlspec/adapters/sqlanywhere/migrations.py +++ b/sqlspec/adapters/sqlanywhere/migrations.py @@ -5,6 +5,9 @@ from contextlib import suppress from typing import TYPE_CHECKING +if TYPE_CHECKING: + from collections.abc import Sequence + from sqlspec.adapters.sqlanywhere.core import ( escape_sql_literal, format_identifier, diff --git a/sqlspec/adapters/sybase/_typing.py b/sqlspec/adapters/sybase/_typing.py index 179925730..1b7337670 100644 --- a/sqlspec/adapters/sybase/_typing.py +++ b/sqlspec/adapters/sybase/_typing.py @@ -14,6 +14,11 @@ PYMSSQL_MODULE: Any = None _DRIVER_CLS: Any = None + +class _SybaseUnavailableError(Exception): + """Fallback Sybase ASE exception base when pymssql is not installed.""" + + if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType @@ -35,7 +40,7 @@ SybaseQueryParams = Any SybaseConnection = Any SybaseRawCursor = Any - SybaseError = Exception + SybaseError = import_optional_attr("pymssql", "Error") or _SybaseUnavailableError __all__ = ( "PYMSSQL_MODULE", diff --git a/sqlspec/adapters/sybase/adk/store.py b/sqlspec/adapters/sybase/adk/store.py index d88720ecc..d009c1f78 100644 --- a/sqlspec/adapters/sybase/adk/store.py +++ b/sqlspec/adapters/sybase/adk/store.py @@ -480,7 +480,7 @@ def insert_memory_entries(self, entries: "list[StoredMemory]", owner_id: "object with self._config.provide_connection() as conn, SybaseCursor(conn, as_dict=False) as cursor: try: for entry in entries: - params: tuple[Any, ...] = ( + params: list[Any] = [ entry["id"], entry["session_id"], entry["app_name"], @@ -492,10 +492,11 @@ def insert_memory_entries(self, entries: "list[StoredMemory]", owner_id: "object to_json(entry["content_json"]), entry["content_text"], to_json(entry["metadata_json"]) if entry.get("metadata_json") is not None else None, - ) + ] if self._owner_id_column_name: - params = (*params, owner_id) - cursor.execute(sql, (*params, entry["event_id"])) + params.append(owner_id) + params.append(entry["event_id"]) + cursor.execute(sql, tuple(params)) inserted += resolve_rowcount(cursor) except Exception: conn.rollback() diff --git a/sqlspec/adapters/sybase/config.py b/sqlspec/adapters/sybase/config.py index 67816299c..b706c651f 100644 --- a/sqlspec/adapters/sybase/config.py +++ b/sqlspec/adapters/sybase/config.py @@ -35,6 +35,8 @@ class SybaseConnectionParams(TypedDict): server: NotRequired[str] host: NotRequired[str] + freetds_server: NotRequired[str] + server_alias: NotRequired[str] user: NotRequired[str] username: NotRequired[str] password: NotRequired[str] diff --git a/sqlspec/adapters/sybase/data_dictionary.py b/sqlspec/adapters/sybase/data_dictionary.py index 8492b2d8e..2dc440724 100644 --- a/sqlspec/adapters/sybase/data_dictionary.py +++ b/sqlspec/adapters/sybase/data_dictionary.py @@ -69,13 +69,17 @@ def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetad column_default=str(r["column_default"]) if r.get("column_default") is not None else None, ordinal_position=int(r.get("ordinal_position", 1)), max_length=int(r.get("max_length", 0)) if r.get("max_length") is not None else 0, + numeric_precision=int(r.get("numeric_precision", 0)) if r.get("numeric_precision") is not None else 0, numeric_scale=int(r.get("numeric_scale", 0)) if r.get("numeric_scale") is not None else 0, is_primary=bool(r.get("is_primary")), is_unique=bool(r.get("is_unique")), + is_identity=bool(r.get("is_identity")), identity_generation=str(r["identity_generation"]) if r.get("identity_generation") else ("ALWAYS" if r.get("is_identity") else None), is_generated=bool(r.get("is_generated")), + generation_expression=str(r["generation_expression"]) if r.get("generation_expression") is not None else None, + is_hidden=bool(r.get("is_hidden")), ) for r in rows ] diff --git a/sqlspec/adapters/sybase/events/store.py b/sqlspec/adapters/sybase/events/store.py index 8f91608df..d5871ac5d 100644 --- a/sqlspec/adapters/sybase/events/store.py +++ b/sqlspec/adapters/sybase/events/store.py @@ -28,6 +28,34 @@ def _integer_type(self) -> str: def _timestamp_default(self) -> str: return "GETUTCDATE()" + def _table_ddl(self) -> str: + """Build Sybase ASE-compatible CREATE TABLE SQL with DEFAULT before NOT NULL and explicit NULL.""" + payload_type, metadata_type, timestamp_type = self._column_types() + string_64 = self._string_type(64) + string_128 = self._string_type(128) + string_32 = self._string_type(32) + integer_type = self._integer_type() + ts_default = self._timestamp_default() + pk_inline = self._primary_key_syntax() + table_clause = self._table_clause() + + pk_column = " PRIMARY KEY" if not pk_inline else "" + + return ( + f"CREATE TABLE {self.table_name} (" + f"event_id {string_64}{pk_column} NOT NULL, " + f"channel {string_128} NOT NULL, " + f"payload_json {payload_type} NOT NULL, " + f"metadata_json {metadata_type} NULL, " + f"status {string_32} DEFAULT 'pending' NOT NULL, " + f"available_at {timestamp_type} DEFAULT {ts_default} NOT NULL, " + f"lease_expires_at {timestamp_type} NULL, " + f"attempts {integer_type} DEFAULT 0 NOT NULL, " + f"created_at {timestamp_type} DEFAULT {ts_default} NOT NULL, " + f"acknowledged_at {timestamp_type} NULL" + f"){pk_inline}{table_clause}" + ) + def _wrap_create_statement(self, statement: str, object_type: str) -> str: object_name = escape_sql_literal(_object_name(self.table_name)) escaped_statement = escape_sql_literal(statement.rstrip().rstrip(";")) diff --git a/sqlspec/adapters/sybase/migrations.py b/sqlspec/adapters/sybase/migrations.py index c3e41c17b..800c66497 100644 --- a/sqlspec/adapters/sybase/migrations.py +++ b/sqlspec/adapters/sybase/migrations.py @@ -5,6 +5,9 @@ from contextlib import suppress from typing import TYPE_CHECKING +if TYPE_CHECKING: + from collections.abc import Sequence + from sqlspec.adapters.sybase.core import ( escape_sql_literal, format_identifier, diff --git a/sqlspec/adapters/sybase/pool.py b/sqlspec/adapters/sybase/pool.py index b97db7e9c..c409f3484 100644 --- a/sqlspec/adapters/sybase/pool.py +++ b/sqlspec/adapters/sybase/pool.py @@ -11,7 +11,7 @@ from sqlspec.adapters.sybase._typing import PYMSSQL_MODULE, SybaseConnection from sqlspec.adapters.sybase.core import DEFAULT_SYBASE_CONN_PROPERTIES, DEFAULT_SYBASE_TDS_VERSION -from sqlspec.exceptions import MissingDependencyError +from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError from sqlspec.utils.logging import POOL_LOGGER_NAME, get_logger, log_with_context from sqlspec.utils.uuids import uuid4 @@ -22,7 +22,6 @@ logger = get_logger(POOL_LOGGER_NAME) _ADAPTER_NAME = "sybase" -_TDSVER_LOCK = threading.RLock() pymssql = PYMSSQL_MODULE @@ -68,6 +67,21 @@ def __init__( on_connection_create: Callback executed when a connection is created. """ params = dict(connection_parameters) + freetds_alias = ( + params.get("freetds_server") + or params.get("server_alias") + or params.get("is_server_alias") + ) + if freetds_alias and isinstance(freetds_alias, str): + params["server"] = freetds_alias + tdsver = os.environ.get("TDSVER") + if not freetds_alias and tdsver != "5.0": + msg = ( + "Sybase ASE requires FreeTDS TDS 5.0 protocol negotiation. " + "Either configure a FreeTDS server section alias via 'server_alias' (or 'freetds_server'), " + "or set the environment variable TDSVER=5.0 before process startup." + ) + raise ImproperConfigurationError(msg) self._tds_version = str( params.pop("_sybase_tds_version", params.pop("tds_version", DEFAULT_SYBASE_TDS_VERSION)) ) @@ -93,26 +107,13 @@ def _database_name(self) -> str: def new_connection(self) -> SybaseConnection: """Open a standalone Sybase ASE connection using FreeTDS TDS 5.0 negotiation. - Temporarily sets ``os.environ['TDSVER']`` inside a module-level re-entrant - lock so FreeTDS ``libsybdb`` sends a native TDS 5.0 login packet without - passing ``tds_version='5.0'`` through ``pymssql._mssql``'s Cython validator. - Returns: SybaseConnection: A newly opened, fully configured connection. """ module = pymssql if module is None: module = _load_pymssql_module() - with _TDSVER_LOCK: - prior_tdsver = os.environ.get("TDSVER") - os.environ["TDSVER"] = self._tds_version - try: - connection = module.connect(**self._connection_parameters) - finally: - if prior_tdsver is None: - os.environ.pop("TDSVER", None) - else: - os.environ["TDSVER"] = prior_tdsver + connection = module.connect(**self._connection_parameters) if self._on_connection_create is not None: self._on_connection_create(connection) diff --git a/tests/unit/adapters/test_sqlanywhere.py b/tests/unit/adapters/test_sqlanywhere.py index 8962f072a..06da062cb 100644 --- a/tests/unit/adapters/test_sqlanywhere.py +++ b/tests/unit/adapters/test_sqlanywhere.py @@ -467,7 +467,7 @@ def _capture(_self: SqlanywhereADKStore, sql: str, params: "tuple[Any, ...]" = ( adk_store.list_sessions("app", "u1", order_by="create_time", descending=False, limit=10, offset=20) sql, params = captured[0] normalized_sql = " ".join(sql.split()) - assert "ORDER BY create_time ASC, id ASC ROWS LIMIT ? OFFSET ?" in normalized_sql + assert "ORDER BY create_time ASC, id ASC LIMIT ? OFFSET ?" in normalized_sql assert params == ("app", "u1", 10, 20) monkeypatch.setattr(SqlanywhereADKMemoryStore, "insert_memory_entries", MagicMock(return_value=1)) diff --git a/tests/unit/adapters/test_sybase.py b/tests/unit/adapters/test_sybase.py index f8998d2b1..ef23069b8 100644 --- a/tests/unit/adapters/test_sybase.py +++ b/tests/unit/adapters/test_sybase.py @@ -38,6 +38,7 @@ DataError, DeadlockError, ForeignKeyViolationError, + ImproperConfigurationError, NotFoundError, NotNullViolationError, OperationalError, @@ -147,39 +148,42 @@ def test_build_connection_config_defaults_and_normalization() -> None: assert "extra" not in cfg -def test_sybase_pool_sets_and_restores_tdsver_env(monkeypatch: pytest.MonkeyPatch) -> None: - """SybaseConnectionPool sets TDSVER='5.0' during connect and restores the prior environment value.""" +def test_sybase_pool_requires_tdsver_or_server_alias(monkeypatch: pytest.MonkeyPatch) -> None: + """SybaseConnectionPool requires either TDSVER='5.0' or a server_alias at process start.""" import sqlspec.adapters.sybase.pool as pool_module - captured_tdsver: list[str | None] = [] - captured_kwargs: list[dict[str, Any]] = [] fake_conn = FakeConnection() class FakeModule: def connect(self, **kwargs: Any) -> FakeConnection: - captured_tdsver.append(os.environ.get("TDSVER")) - captured_kwargs.append(kwargs) return fake_conn monkeypatch.setattr(pool_module, "pymssql", FakeModule()) - monkeypatch.setenv("TDSVER", "7.3") - cfg = build_connection_config({"server": "ase.example.test", "user": "sa", "database": "master"}) - pool = SybaseConnectionPool(cfg) - conn = pool.acquire() - pool.release(conn) + monkeypatch.delenv("TDSVER", raising=False) + cfg_invalid = build_connection_config({"server": "ase.example.test", "user": "sa", "database": "master"}) + with pytest.raises(ImproperConfigurationError): + SybaseConnectionPool(cfg_invalid) + monkeypatch.setenv("TDSVER", "5.0") + cfg_env = build_connection_config({"server": "ase.example.test", "user": "sa", "database": "master"}) + pool_env = SybaseConnectionPool(cfg_env) + conn = pool_env.acquire() + pool_env.release(conn) assert cast("object", conn) is fake_conn - assert captured_tdsver == ["5.0"] - assert os.environ.get("TDSVER") == "7.3" - assert "tds_version" not in captured_kwargs[0] - assert "_sybase_tds_version" not in captured_kwargs[0] - assert captured_kwargs[0]["conn_properties"] == DEFAULT_SYBASE_CONN_PROPERTIES - assert captured_kwargs[0]["autocommit"] is True + assert os.environ.get("TDSVER") == "5.0" + monkeypatch.delenv("TDSVER", raising=False) + cfg_alias = build_connection_config({"server_alias": "SYBASE_ASE", "user": "sa", "database": "master"}) + pool_alias = SybaseConnectionPool(cfg_alias) + conn_alias = pool_alias.acquire() + pool_alias.release(conn_alias) + assert cast("object", conn_alias) is fake_conn -def test_sybase_pool_transmits_native_tds_50_login_packet_over_loopback() -> None: + +def test_sybase_pool_transmits_native_tds_50_login_packet_over_loopback(monkeypatch: pytest.MonkeyPatch) -> None: """SybaseConnectionPool.new_connection transmits a native 619-byte TDS 5.0 login packet via FreeTDS.""" + monkeypatch.setenv("TDSVER", "5.0") received_packets: list[bytes] = [] ready = threading.Event() From 2f297f52ab2ed58386769e0ef7d7afb974338fdd Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 01:44:42 +0000 Subject: [PATCH 33/35] docs(databases): document sybase and sqlanywhere database setup and dialect workflows --- docs/databases/index.md | 4 + docs/databases/sqlanywhere.md | 53 +++++++++ docs/databases/sybase.md | 82 ++++++++++++++ .../adapters/sqlanywhere/data_dictionary.py | 4 +- sqlspec/adapters/sqlanywhere/migrations.py | 8 +- sqlspec/adapters/sybase/data_dictionary.py | 4 +- sqlspec/adapters/sybase/migrations.py | 8 +- sqlspec/adapters/sybase/pool.py | 6 +- sqlspec/dialects/sybase/_generators.py | 4 +- sqlspec/dialects/sybase/_parsers.py | 11 +- sqlspec/utils/fixtures.py | 106 +++++++++++------- tests/unit/dialects/test_sqlanywhere.py | 4 +- tests/unit/utils/test_fixtures.py | 48 +++++++- 13 files changed, 267 insertions(+), 75 deletions(-) create mode 100644 docs/databases/sqlanywhere.md create mode 100644 docs/databases/sybase.md diff --git a/docs/databases/index.md b/docs/databases/index.md index ec4e5ea4b..2bac5e867 100644 --- a/docs/databases/index.md +++ b/docs/databases/index.md @@ -20,6 +20,8 @@ SQLSpec supports synchronous and asynchronous **database adapters** spanning emb | [`oracledb`](oracledb.md) | `sqlspec[oracledb]` | Sync + Async | `:1` / `:name` | `ConnectionPool` / `AsyncConnectionPool` + DRCP | PyCapsule Batches | Oracle AQ (`DBMS_AQ`) + Table Queue | Sessions + Memory (Oracle 26ai `VECTOR` HNSW, Oracle Text, RRF) | | [`mssql_python`](mssql.md) | `sqlspec[mssql-python]` | Sync | `?` (`qmark`) | `MssqlPythonConnectionPool` | Native `BulkCopy` | Table Queue | Sessions + Memory (`LIKE`) | | [`pymssql`](mssql.md) | `sqlspec[pymssql]` | Sync | `%(name)s` / `%s` | `PymssqlConnectionPool` | Row | Table Queue | Sessions + Memory (`LIKE`) | +| [`sybase`](sybase.md) | `sqlspec[sybase]` | Sync | `?` (`qmark`) / `%(name)s` | `SybaseConnectionPool` | Row / Stream | Table Queue | Sessions + Memory (`LIKE`) | +| [`sqlanywhere`](sqlanywhere.md) | `sqlspec[sqlanywhere]` | Sync | `?` (`qmark`) | `SqlanywhereConnectionPool` | Row / Stream | Table Queue | Sessions + Memory (`LIKE`) | | [`db2`](db2.md) | `sqlspec[db2]` | Sync + Async | `?` (`qmark`) | `Db2SyncConnectionPool` / `Db2AsyncConnectionPool` | Schema-Guided Row | Table Queue (`SKIP LOCKED DATA`) | Sessions + Memory (`POSSTR`) | | [`duckdb`](duckdb.md) | `sqlspec[duckdb]` | Sync | `?` (`qmark`) | `DuckDBConnectionPool` | Native Zero-Copy | Table Queue | Sessions + Memory (`FTS`, `ARRAY` Cosine) | | [`bigquery`](bigquery.md) | `sqlspec[bigquery]` | Sync | `@name` (`named`) | Stateless HTTP/gRPC Client | Storage Read / Write API | Table Queue | Sessions + Memory (`SEARCH` / `LIKE`) | @@ -35,6 +37,8 @@ SQLSpec supports synchronous and asynchronous **database adapters** spanning emb - **[MySQL and MariaDB](mysql.md)**: Side-by-side setup for `asyncmy`, `pymysql`, `aiomysql`, and `mysql-connector-python`, TLS verification, and `LOAD DATA LOCAL INFILE` bulk loading. - **[Oracle Database](oracledb.md)**: Thin and Thick modes, mTLS/PKCS#12 wallets, TNS topologies, Oracle 26ai `VECTOR` + Hybrid RRF search, PyCapsule Arrow streaming, and `DBMS_CLOUD.COPY_DATA`. - **[Microsoft SQL Server](mssql.md)**: Microsoft's official `mssql-python` driver with streaming Arrow `BulkCopy` alongside FreeTDS `pymssql`. +- **[SAP Adaptive Server Enterprise](sybase.md)**: Sybase ASE 16.0+ via FreeTDS TDS 5.0 (`pymssql`), transactional DDL behavior, and catalog introspection. +- **[SAP SQL Anywhere](sqlanywhere.md)**: Embedded and client-server SQL Anywhere 12+ via `sqlanydb`, recursive dependency analysis, and automatic rollback on context exit. - **[IBM Db2](db2.md)**: Sync and async DRDA pooling (`Db2SyncConfig` / `Db2AsyncConfig`), savepoint cursor retention, and lowercase column normalization. - **[DuckDB](duckdb.md)**: In-process OLAP queries over Parquet/S3, hybrid native/Arrow materialization, and JSON statement profiling. - **[Google Cloud BigQuery](bigquery.md)**: Interactive `query_and_wait` execution, Storage Read/Write APIs, reservation governance, and multi-statement script jobs. diff --git a/docs/databases/sqlanywhere.md b/docs/databases/sqlanywhere.md new file mode 100644 index 000000000..9c2393dbc --- /dev/null +++ b/docs/databases/sqlanywhere.md @@ -0,0 +1,53 @@ +# SAP SQL Anywhere + +`sqlspec.adapters.sqlanywhere` provides synchronous database connectivity for SAP SQL Anywhere 12+ through the official `sqlanydb` driver (`>= 1.0.14`), offering connection pooling (`SqlanywhereConfig`), custom SQLGlot `sqlanywhere` dialect compilation, recursive foreign-key dependency analysis, and Litestar, Events, and Google ADK session/memory store extensions. + +## Install and When to Choose + +```bash +uv add "sqlspec[sqlanywhere]" +``` + +- Choose **`SqlanywhereConfig`** for embedded, edge, and client-server applications interacting with SAP SQL Anywhere database engines. +- `sqlanydb` wraps SAP's native client C library (`dbcapi`) via `ctypes`. Ensure `libdbcapi_r.so` (Linux), `libdbcapi_r.dylib` (macOS), or `dbcapi.dll` (Windows) is installed and available in the system library path. + +## Minimal Connection and Pool Setup + +```python +from sqlspec import SQLSpec +from sqlspec.adapters.sqlanywhere import SqlanywhereConfig + +spec = SQLSpec() +config = spec.add_config( + SqlanywhereConfig( + connection_config={ + "uid": "dba", + "pwd": "sql", + "eng": "demo_server", + "dbn": "demo", + "host": "localhost:2638", + } + ) +) +``` + +## Key Configuration Parameters + +| Setting | Location | Description | +| --- | --- | --- | +| `uid` / `user` | `connection_config` | Database user name (default DBA administrative principal). | +| `pwd` / `password` | `connection_config` | Database user password. | +| `eng` / `server` | `connection_config` | Running SQL Anywhere database engine name. | +| `dbn` / `database` | `connection_config` | Target database catalog name. | +| `host` | `connection_config` | Network host and port string (`host:port`, default port 2638). | +| `dbf` | `connection_config` | Direct database file path for auto-starting engines. | +| `pool_recycle_seconds` | `connection_config` | Maximum connection age in seconds before recycling. | +| `health_check_interval` | `connection_config` | Idle duration in seconds before running ping probes. | +| `json_serializer` / `json_deserializer` | `driver_features` | Custom JSON serialization/deserialization callables. | + +## Dialect and Catalog Notes + +- **Automatic Rollback on Context Exit**: SQL Anywhere connections maintain an implicit transaction upon statement execution. `SqlanywhereSessionContext` automatically rolls back uncommitted changes if a session exits with open work or an error, ensuring clean connection return to the pool. +- **Pagination**: Supports standard suffix `SELECT ... LIMIT n OFFSET m` as well as SQL Anywhere `TOP n START AT (m + 1)` syntax. +- **Identity Resync**: Fixture seeding supports primary key identity resetting via `CALL sa_reset_identity('', '', )`. +- **System Catalogs**: Schema discovery queries `SYS.SYSTAB`, `SYS.SYSTABCOL`, `SYS.SYSIDX`, `SYS.SYSIDXCOL`, `SYS.SYSFKEY`, and `SYS.SYSDOMAIN`. diff --git a/docs/databases/sybase.md b/docs/databases/sybase.md new file mode 100644 index 000000000..23d9b8838 --- /dev/null +++ b/docs/databases/sybase.md @@ -0,0 +1,82 @@ +# SAP Adaptive Server Enterprise (Sybase ASE) + +`sqlspec.adapters.sybase` provides first-class support for SAP Adaptive Server Enterprise (SAP ASE) 16.0+ via `pymssql` (`>= 2.4.4`) over FreeTDS TDS 5.0 wire protocol, featuring connection pooling (`SybaseConfig`), custom SQLGlot `sybase` dialect compilation, system catalog introspection, and Litestar, Events, and Google ADK session/memory store extensions. + +## Install and When to Choose + +```bash +uv add "sqlspec[sybase]" +``` + +- Choose **`SybaseConfig`** for transactional OLTP applications communicating with SAP ASE database instances. +- Prebuilt `pymssql` wheels ship statically linked FreeTDS with native TDS 5.0 wire support across Linux, macOS, and Windows. +- Note: Neither Apache Arrow ADBC nor GizmoData provides native ADBC drivers for SAP ASE, so SQLSpec communicates with ASE directly via FreeTDS TDS 5.0 DB-API connections. + +## Connection Setup and Protocol Negotiation + +Sybase ASE requires TDS 5.0 protocol negotiation. FreeTDS negotiates TDS 5.0 either when an environment variable `TDSVER=5.0` is set before process start, or when a FreeTDS server section alias is defined in `freetds.conf`: + +=== "Environment Variable (`TDSVER=5.0`)" + + ```bash + export TDSVER=5.0 + ``` + + ```python + from sqlspec import SQLSpec + from sqlspec.adapters.sybase import SybaseConfig + + spec = SQLSpec() + config = spec.add_config( + SybaseConfig( + connection_config={ + "server": "ase.example.internal", + "port": 5000, + "user": "sa", + "password": "secret_password", + "database": "app_db", + "autocommit": True, + } + ) + ) + ``` + +=== "FreeTDS Server Alias (`server_alias`)" + + ```python + from sqlspec import SQLSpec + from sqlspec.adapters.sybase import SybaseConfig + + spec = SQLSpec() + config = spec.add_config( + SybaseConfig( + connection_config={ + "server_alias": "SYBASE_PROD", + "user": "sa", + "password": "secret_password", + "database": "app_db", + } + ) + ) + ``` + +## Key Configuration Parameters + +| Setting | Location | Description | +| --- | --- | --- | +| `server` / `host` | `connection_config` | Database host name or IP address (default port `5000`). | +| `server_alias` / `freetds_server` | `connection_config` | Section name in `freetds.conf` configured with `tds version = 5.0`. | +| `database` / `db` | `connection_config` | Target database catalog name. | +| `user` / `username` | `connection_config` | Authentication login principal. | +| `password` | `connection_config` | Authentication password credentials. | +| `autocommit` | `connection_config` | Defaults to `True` to allow DDL execution outside implicit transactions. | +| `conn_properties` | `connection_config` | Sybase session initialization commands (defaults to quoted identifier and ANSINULL options). | +| `pool_recycle_seconds` | `connection_config` | Maximum connection age in seconds before recycling (defaults to 24 hours). | +| `health_check_interval` | `connection_config` | Minimum idle time in seconds before running liveness probes. | +| `json_serializer` / `json_deserializer` | `driver_features` | Custom JSON text serialization/deserialization callables. | + +## Dialect and Catalog Notes + +- **Autocommit and DDL in Transactions**: In Sybase ASE, statements like `CREATE TABLE` and `CREATE INDEX` raise error 2762 inside multi-statement transactions unless database option `ddl in tran` is enabled. `SybaseConfig` defaults `autocommit=True` and begins explicit transactions on demand via `driver.begin()`. +- **Pagination**: Supports both unparenthesized `SELECT TOP n ...` and ANSI SQL pagination `ROWS LIMIT n OFFSET m`. +- **System Catalogs**: Reflection queries introspect `dbo.sysobjects`, `dbo.syscolumns`, `dbo.sysindexes`, `dbo.sysreferences`, and `dbo.systypes`. diff --git a/sqlspec/adapters/sqlanywhere/data_dictionary.py b/sqlspec/adapters/sqlanywhere/data_dictionary.py index dce3659cb..4743347cc 100644 --- a/sqlspec/adapters/sqlanywhere/data_dictionary.py +++ b/sqlspec/adapters/sqlanywhere/data_dictionary.py @@ -82,7 +82,9 @@ def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetad if r.get("identity_generation") else ("ALWAYS" if r.get("is_identity") else None), is_generated=bool(r.get("is_generated")), - generation_expression=str(r["generation_expression"]) if r.get("generation_expression") is not None else None, + generation_expression=str(r["generation_expression"]) + if r.get("generation_expression") is not None + else None, is_hidden=bool(r.get("is_hidden")), ) for r in rows diff --git a/sqlspec/adapters/sqlanywhere/migrations.py b/sqlspec/adapters/sqlanywhere/migrations.py index 8ff433580..767249bee 100644 --- a/sqlspec/adapters/sqlanywhere/migrations.py +++ b/sqlspec/adapters/sqlanywhere/migrations.py @@ -133,13 +133,7 @@ def record_migration( ) else: statement = self._record_migration_statement( - version, - version_type, - next_sequence, - description, - execution_time_ms, - checksum, - applied_by, + version, version_type, next_sequence, description, execution_time_ms, checksum, applied_by ) driver.execute(statement) if commit: diff --git a/sqlspec/adapters/sybase/data_dictionary.py b/sqlspec/adapters/sybase/data_dictionary.py index 2dc440724..1c8200f2b 100644 --- a/sqlspec/adapters/sybase/data_dictionary.py +++ b/sqlspec/adapters/sybase/data_dictionary.py @@ -78,7 +78,9 @@ def build_column_metadata(rows: "Iterable[dict[str, Any]]") -> "list[ColumnMetad if r.get("identity_generation") else ("ALWAYS" if r.get("is_identity") else None), is_generated=bool(r.get("is_generated")), - generation_expression=str(r["generation_expression"]) if r.get("generation_expression") is not None else None, + generation_expression=str(r["generation_expression"]) + if r.get("generation_expression") is not None + else None, is_hidden=bool(r.get("is_hidden")), ) for r in rows diff --git a/sqlspec/adapters/sybase/migrations.py b/sqlspec/adapters/sybase/migrations.py index 800c66497..1e0a4e613 100644 --- a/sqlspec/adapters/sybase/migrations.py +++ b/sqlspec/adapters/sybase/migrations.py @@ -131,13 +131,7 @@ def record_migration( ) else: statement = self._record_migration_statement( - version, - version_type, - next_sequence, - description, - execution_time_ms, - checksum, - applied_by, + version, version_type, next_sequence, description, execution_time_ms, checksum, applied_by ) driver.execute(statement) if commit: diff --git a/sqlspec/adapters/sybase/pool.py b/sqlspec/adapters/sybase/pool.py index c409f3484..fe16526f7 100644 --- a/sqlspec/adapters/sybase/pool.py +++ b/sqlspec/adapters/sybase/pool.py @@ -67,11 +67,7 @@ def __init__( on_connection_create: Callback executed when a connection is created. """ params = dict(connection_parameters) - freetds_alias = ( - params.get("freetds_server") - or params.get("server_alias") - or params.get("is_server_alias") - ) + freetds_alias = params.get("freetds_server") or params.get("server_alias") or params.get("is_server_alias") if freetds_alias and isinstance(freetds_alias, str): params["server"] = freetds_alias tdsver = os.environ.get("TDSVER") diff --git a/sqlspec/dialects/sybase/_generators.py b/sqlspec/dialects/sybase/_generators.py index 0363faad6..4ea032b2a 100644 --- a/sqlspec/dialects/sybase/_generators.py +++ b/sqlspec/dialects/sybase/_generators.py @@ -267,9 +267,7 @@ def sqlanywhere_generated_identity_sql( return "DEFAULT AUTOINCREMENT" -def sqlanywhere_autoincrement_sql( - gen: "generator.Generator", expression: exp.AutoIncrementColumnConstraint -) -> str: +def sqlanywhere_autoincrement_sql(gen: "generator.Generator", expression: exp.AutoIncrementColumnConstraint) -> str: """Render AutoIncrementColumnConstraint as DEFAULT AUTOINCREMENT.""" _ = (gen, expression) return "DEFAULT AUTOINCREMENT" diff --git a/sqlspec/dialects/sybase/_parsers.py b/sqlspec/dialects/sybase/_parsers.py index 461874d0d..fe6d51940 100644 --- a/sqlspec/dialects/sybase/_parsers.py +++ b/sqlspec/dialects/sybase/_parsers.py @@ -174,12 +174,19 @@ def _rewrite_top_start_at_statement(statement: "list[Token]") -> "list[Token]": offset_str = str(max(int(start_val_tok.text) - 1, 0)) offset_tokens = [ Token( - TokenType.NUMBER, offset_str, start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end + TokenType.NUMBER, + offset_str, + start_val_tok.line, + start_val_tok.col, + start_val_tok.start, + start_val_tok.end, ) ] else: offset_tokens = [ - Token(TokenType.L_PAREN, "(", start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.start), + Token( + TokenType.L_PAREN, "(", start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.start + ), start_val_tok, Token(TokenType.DASH, "-", start_val_tok.line, start_val_tok.col, start_val_tok.start, start_val_tok.end), Token(TokenType.NUMBER, "1", start_val_tok.line, start_val_tok.col, start_val_tok.end, start_val_tok.end), diff --git a/sqlspec/utils/fixtures.py b/sqlspec/utils/fixtures.py index 4706f2a42..a1e0b378c 100644 --- a/sqlspec/utils/fixtures.py +++ b/sqlspec/utils/fixtures.py @@ -2006,34 +2006,46 @@ def _sequence_resync_statements( return statements +def _resync_sybase_sequences_sync(driver: "SyncDriverAdapterBase", table: str, columns: "list[_TableColumn]") -> None: + """Resynchronize Sybase ASE identity columns for a table synchronously.""" + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = driver.select_value(query) + next_val = int(max_val or 0) + if next_val > 0: + escaped_table = table.replace("'", "''") + driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + + +def _resync_sqlanywhere_sequences_sync( + driver: "SyncDriverAdapterBase", table: str, columns: "list[_TableColumn]" +) -> None: + """Resynchronize SAP SQL Anywhere identity columns for a table synchronously.""" + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = driver.select_value(query) + next_val = int(max_val or 0) + schema_part, _, table_part = table.rpartition(".") + escaped_table = table_part.replace("'", "''") + escaped_schema = schema_part.replace("'", "''") + schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" + driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + + def _resync_table_sequences_sync( driver: "SyncDriverAdapterBase", family: str, dialect_name: str, table: str, columns: "list[_TableColumn]" ) -> None: """Resynchronize sequences, identity columns, or auto-increment counters for a table synchronously.""" if family == "sybase": - for column in columns: - if column.is_identity: - quoted_col = _quoted_identifier_sql(column.name) - query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" - max_val = driver.select_value(query) - next_val = int(max_val or 0) - if next_val > 0: - escaped_table = table.replace("'", "''") - driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + _resync_sybase_sequences_sync(driver, table, columns) return if family == "sqlanywhere": - for column in columns: - if column.is_identity: - quoted_col = _quoted_identifier_sql(column.name) - query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" - max_val = driver.select_value(query) - next_val = int(max_val or 0) - schema_part, _, table_part = table.rpartition(".") - escaped_table = table_part.replace("'", "''") - escaped_schema = schema_part.replace("'", "''") - schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" - driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + _resync_sqlanywhere_sequences_sync(driver, table, columns) return if family == "db2": @@ -2132,34 +2144,48 @@ def _resync_table_sequences_sync( driver.execute(statement, parameters) +async def _resync_sybase_sequences_async( + driver: "AsyncDriverAdapterBase", table: str, columns: "list[_TableColumn]" +) -> None: + """Resynchronize Sybase ASE identity columns for a table asynchronously.""" + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = await driver.select_value(query) + next_val = int(max_val or 0) + if next_val > 0: + escaped_table = table.replace("'", "''") + await driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + + +async def _resync_sqlanywhere_sequences_async( + driver: "AsyncDriverAdapterBase", table: str, columns: "list[_TableColumn]" +) -> None: + """Resynchronize SAP SQL Anywhere identity columns for a table asynchronously.""" + for column in columns: + if column.is_identity: + quoted_col = _quoted_identifier_sql(column.name) + query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" + max_val = await driver.select_value(query) + next_val = int(max_val or 0) + schema_part, _, table_part = table.rpartition(".") + escaped_table = table_part.replace("'", "''") + escaped_schema = schema_part.replace("'", "''") + schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" + await driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + + async def _resync_table_sequences_async( driver: "AsyncDriverAdapterBase", family: str, dialect_name: str, table: str, columns: "list[_TableColumn]" ) -> None: """Resynchronize sequences, identity columns, or auto-increment counters for a table asynchronously.""" if family == "sybase": - for column in columns: - if column.is_identity: - quoted_col = _quoted_identifier_sql(column.name) - query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" - max_val = await driver.select_value(query) - next_val = int(max_val or 0) - if next_val > 0: - escaped_table = table.replace("'", "''") - await driver.execute(f"EXEC sp_chgattribute '{escaped_table}', 'identity_burn_max', 0, '{next_val}'") + await _resync_sybase_sequences_async(driver, table, columns) return if family == "sqlanywhere": - for column in columns: - if column.is_identity: - quoted_col = _quoted_identifier_sql(column.name) - query = f"SELECT coalesce(max({quoted_col}), 0) FROM {_quoted_table_name(table)}" - max_val = await driver.select_value(query) - next_val = int(max_val or 0) - schema_part, _, table_part = table.rpartition(".") - escaped_table = table_part.replace("'", "''") - escaped_schema = schema_part.replace("'", "''") - schema_sql = f"'{escaped_schema}'" if schema_part else "NULL" - await driver.execute(f"CALL sa_reset_identity('{escaped_table}', {schema_sql}, {next_val})") + await _resync_sqlanywhere_sequences_async(driver, table, columns) return if family == "db2": diff --git a/tests/unit/dialects/test_sqlanywhere.py b/tests/unit/dialects/test_sqlanywhere.py index 9c586794d..c7221a7ac 100644 --- a/tests/unit/dialects/test_sqlanywhere.py +++ b/tests/unit/dialects/test_sqlanywhere.py @@ -99,9 +99,7 @@ def test_sqlanywhere_parse_top_start_at_non_literal() -> None: def test_sqlanywhere_current_timestamp_local_and_utc() -> None: """Verify CURRENT_TIMESTAMP renders local and GETUTCDATE renders UTC.""" - assert ( - transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sqlanywhere")[0] == "SELECT CURRENT TIMESTAMP" - ) + assert transpile("SELECT CURRENT_TIMESTAMP", read="postgres", write="sqlanywhere")[0] == "SELECT CURRENT TIMESTAMP" assert transpile("SELECT GETUTCDATE()", read="tsql", write="sqlanywhere")[0] == "SELECT CURRENT UTC TIMESTAMP" diff --git a/tests/unit/utils/test_fixtures.py b/tests/unit/utils/test_fixtures.py index abe680eb6..0890b8eb9 100644 --- a/tests/unit/utils/test_fixtures.py +++ b/tests/unit/utils/test_fixtures.py @@ -2757,8 +2757,20 @@ def test_sybase_identity_resync_executes_sp_chgattribute_sync(tmp_path: Path) -> driver = MagicMock() driver.statement_config.dialect = "sybase" driver.data_dictionary.get_columns.return_value = [ - {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, - {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + { + "column_name": "id", + "data_type": "int", + "is_primary": True, + "identity_generation": "ALWAYS", + "is_generated": 0, + }, + { + "column_name": "name", + "data_type": "varchar", + "is_primary": False, + "identity_generation": None, + "is_generated": 0, + }, ] driver.select_value.return_value = 42 @@ -2776,8 +2788,20 @@ def test_sybase_identity_resync_skips_when_max_is_zero(tmp_path: Path) -> None: driver = MagicMock() driver.statement_config.dialect = "sybase" driver.data_dictionary.get_columns.return_value = [ - {"column_name": "id", "data_type": "int", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, - {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + { + "column_name": "id", + "data_type": "int", + "is_primary": True, + "identity_generation": "ALWAYS", + "is_generated": 0, + }, + { + "column_name": "name", + "data_type": "varchar", + "is_primary": False, + "identity_generation": None, + "is_generated": 0, + }, ] driver.select_value.return_value = 0 @@ -2796,8 +2820,20 @@ async def test_sqlanywhere_identity_resync_executes_sa_reset_identity_async(tmp_ driver.statement_config.dialect = "sqlanywhere" driver.data_dictionary.get_columns = AsyncMock( return_value=[ - {"column_name": "id", "data_type": "integer", "is_primary": True, "identity_generation": "ALWAYS", "is_generated": 0}, - {"column_name": "name", "data_type": "varchar", "is_primary": False, "identity_generation": None, "is_generated": 0}, + { + "column_name": "id", + "data_type": "integer", + "is_primary": True, + "identity_generation": "ALWAYS", + "is_generated": 0, + }, + { + "column_name": "name", + "data_type": "varchar", + "is_primary": False, + "identity_generation": None, + "is_generated": 0, + }, ] ) driver.execute_many = AsyncMock() From 74fc6bedba29b39f102b2753e2435716b09c808a Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 02:17:07 +0000 Subject: [PATCH 34/35] docs(adapters): add field docstrings for sybase and sqlanywhere typeddict configs --- sqlspec/adapters/sqlanywhere/config.py | 30 ++++++++++++++++++++++++++ sqlspec/adapters/sybase/config.py | 30 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/sqlspec/adapters/sqlanywhere/config.py b/sqlspec/adapters/sqlanywhere/config.py index 04c159a5e..fe635e405 100644 --- a/sqlspec/adapters/sqlanywhere/config.py +++ b/sqlspec/adapters/sqlanywhere/config.py @@ -37,45 +37,75 @@ class SqlanywhereConnectionParams(TypedDict): """SAP SQL Anywhere connection parameters.""" uid: NotRequired[str] + """User ID / login name for authentication.""" user: NotRequired[str] + """Alias for ``uid``.""" username: NotRequired[str] + """Alias for ``uid``.""" pwd: NotRequired[str] + """Password for authentication.""" password: NotRequired[str] + """Alias for ``pwd``.""" dbn: NotRequired[str] + """Database name on the SQL Anywhere server.""" database: NotRequired[str] + """Alias for ``dbn``.""" db: NotRequired[str] + """Alias for ``dbn``.""" eng: NotRequired[str] + """SQL Anywhere database engine name.""" server: NotRequired[str] + """Alias for ``eng``.""" engine: NotRequired[str] + """Alias for ``eng``.""" server_name: NotRequired[str] + """Alias for ``eng``.""" dbf: NotRequired[str] + """Database file path for local or embedded engines.""" database_file: NotRequired[str] + """Alias for ``dbf``.""" host: NotRequired[str] + """TCP hostname or IP address of the server.""" port: NotRequired[int | str] + """TCP port number of the server.""" dsn: NotRequired[str] + """ODBC data source name.""" links: NotRequired[str] + """Network communications links protocol configuration.""" cset: NotRequired[str] + """Character set used for character data conversion.""" extra: NotRequired["dict[str, Any]"] + """Additional connection parameters passed to the driver.""" class SqlanywherePoolParams(SqlanywhereConnectionParams): """SAP SQL Anywhere pool parameters.""" pool_min_size: NotRequired[int] + """Minimum number of connections maintained in the pool.""" pool_max_size: NotRequired[int] + """Maximum number of connections allowed in the pool.""" pool_timeout: NotRequired[float] + """Seconds to wait for a connection before timing out.""" pool_recycle_seconds: NotRequired[int] + """Maximum age in seconds before a connection is recycled.""" health_check_interval: NotRequired[float] + """Interval in seconds between background health checks.""" class SqlanywhereDriverFeatures(TypedDict): """SAP SQL Anywhere driver feature flags.""" json_serializer: NotRequired["Callable[[Any], str]"] + """Custom callable for serializing objects to JSON strings.""" json_deserializer: NotRequired["Callable[[str], Any]"] + """Custom callable for deserializing JSON strings.""" on_connection_create: "NotRequired[Callable[[SqlanywhereConnection], None]]" + """Callback invoked immediately when a new connection is established.""" enable_events: NotRequired[bool] + """Enable database event listening.""" events_backend: NotRequired[Literal["poll_queue"]] + """Backend mechanism used for dispatching events.""" class SqlanywhereConnectionContext(SyncPoolConnectionContext): diff --git a/sqlspec/adapters/sybase/config.py b/sqlspec/adapters/sybase/config.py index b706c651f..70bac45c2 100644 --- a/sqlspec/adapters/sybase/config.py +++ b/sqlspec/adapters/sybase/config.py @@ -34,30 +34,55 @@ class SybaseConnectionParams(TypedDict): """Sybase ASE connection parameters.""" server: NotRequired[str] + """Database server host, IP, or FreeTDS server definition name.""" host: NotRequired[str] + """Alias for ``server``.""" freetds_server: NotRequired[str] + """FreeTDS server configuration section name.""" server_alias: NotRequired[str] + """Alias for ``server``.""" user: NotRequired[str] + """Login user name.""" username: NotRequired[str] + """Alias for ``user``.""" password: NotRequired[str] + """Authentication password.""" database: NotRequired[str] + """Initial database name to connect to.""" db: NotRequired[str] + """Alias for ``database``.""" port: NotRequired[int | str] + """TCP port number of the Sybase ASE server.""" timeout: NotRequired[SybaseTimeout] + """Query execution timeout in seconds.""" login_timeout: NotRequired[SybaseTimeout] + """Login and connection timeout in seconds.""" charset: NotRequired[str] + """Client character set encoding.""" as_dict: NotRequired[bool] + """Return query result rows as dictionaries.""" appname: NotRequired[str] + """Application name sent to the server for connection identification.""" conn_properties: NotRequired[str] + """Additional connection properties string.""" autocommit: NotRequired[bool] + """Enable autocommit mode for transactions.""" tds_version: NotRequired[str] + """TDS protocol version (such as ``'5.0'`` or ``'7.4'``).""" encryption: NotRequired[Literal["off", "request", "require"]] + """TDS connection encryption level.""" arraysize: NotRequired[int] + """Default cursor fetch array size.""" conv: NotRequired[Mapping[int | type[Any], Callable[..., Any]]] + """Custom datatype conversion dictionary mapping TDS types to python types.""" read_only: NotRequired[bool] + """Enforce read-only transactions on connections.""" pool_recycle_seconds: NotRequired[int] + """Maximum age in seconds before a pooled connection is recycled.""" health_check_interval: NotRequired[float] + """Interval in seconds between background health checks.""" extra: NotRequired["dict[str, Any]"] + """Additional driver connection arguments.""" class SybasePoolParams(SybaseConnectionParams): @@ -68,10 +93,15 @@ class SybaseDriverFeatures(TypedDict): """Sybase ASE driver feature flags.""" json_serializer: NotRequired["Callable[[Any], str]"] + """Custom callable for serializing objects to JSON strings.""" json_deserializer: NotRequired["Callable[[str], Any]"] + """Custom callable for deserializing JSON strings.""" on_connection_create: "NotRequired[Callable[[SybaseConnection], None]]" + """Callback invoked immediately when a new connection is established.""" enable_events: NotRequired[bool] + """Enable database event listening.""" events_backend: NotRequired[Literal["poll_queue"]] + """Backend mechanism used for dispatching events.""" class SybaseConnectionContext(SyncPoolConnectionContext): From 5db9160347d1491e866f81dd82aeacf8e1034ab4 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Sat, 31 Oct 2026 02:28:34 +0000 Subject: [PATCH 35/35] fix(adapters): eliminate eager pymssql import in sybase and sqlanywhere typing --- sqlspec/adapters/sqlanywhere/_typing.py | 6 ++---- sqlspec/adapters/sybase/_typing.py | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/sqlspec/adapters/sqlanywhere/_typing.py b/sqlspec/adapters/sqlanywhere/_typing.py index 8c2b61e2e..d9420b415 100644 --- a/sqlspec/adapters/sqlanywhere/_typing.py +++ b/sqlspec/adapters/sqlanywhere/_typing.py @@ -15,8 +15,8 @@ _DRIVER_CLS: Any = None -class _SqlanywhereUnavailableError(Exception): - """Fallback SQL Anywhere exception base when sqlanydb is not installed.""" +class SqlanywhereError(Exception): + """SAP SQL Anywhere driver exception base.""" if TYPE_CHECKING: @@ -30,13 +30,11 @@ class _SqlanywhereUnavailableError(Exception): SqlanywhereConnection: TypeAlias = Any SqlanywhereRawCursor: TypeAlias = Any SqlanywhereQueryParams: TypeAlias = Any - SqlanywhereError: TypeAlias = Exception if not TYPE_CHECKING: SqlanywhereConnection = Any SqlanywhereRawCursor = Any SqlanywhereQueryParams = Any - SqlanywhereError = import_optional_attr("sqlanydb", "Error") or _SqlanywhereUnavailableError __all__ = ( "SQLANYDB_MODULE", diff --git a/sqlspec/adapters/sybase/_typing.py b/sqlspec/adapters/sybase/_typing.py index 1b7337670..7b8826d9d 100644 --- a/sqlspec/adapters/sybase/_typing.py +++ b/sqlspec/adapters/sybase/_typing.py @@ -15,8 +15,8 @@ _DRIVER_CLS: Any = None -class _SybaseUnavailableError(Exception): - """Fallback Sybase ASE exception base when pymssql is not installed.""" +class SybaseError(Exception): + """Sybase ASE driver exception base.""" if TYPE_CHECKING: @@ -26,7 +26,6 @@ class _SybaseUnavailableError(Exception): from pymssql import Connection as _SybaseConnection from pymssql import Cursor as _SybaseRawCursor - from pymssql import Error as _SybaseError from pymssql._pymssql import QueryParams as SybaseQueryParams from sqlspec.adapters.sybase.driver import SybaseDriver @@ -34,13 +33,11 @@ class _SybaseUnavailableError(Exception): SybaseConnection: TypeAlias = _SybaseConnection SybaseRawCursor: TypeAlias = _SybaseRawCursor - SybaseError: TypeAlias = _SybaseError if not TYPE_CHECKING: SybaseQueryParams = Any SybaseConnection = Any SybaseRawCursor = Any - SybaseError = import_optional_attr("pymssql", "Error") or _SybaseUnavailableError __all__ = ( "PYMSSQL_MODULE",