diff --git a/docs/conf.py b/docs/conf.py index 30570d787..bf2dc8e93 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -135,6 +135,7 @@ "ParameterMapping": "sqlspec.core.parameters.ParameterMapping", "ParameterSequence": "sqlspec.core.parameters.ParameterSequence", "ParameterPayload": "sqlspec.core.parameters.ParameterPayload", + "DialectType": "sqlglot.dialects.dialect.DialectType", "Union": "typing.Union", "Callable": "typing.Callable", "Any": "typing.Any", diff --git a/docs/reference/core/filters.rst b/docs/reference/core/filters.rst index 17217289e..ea5cc981e 100644 --- a/docs/reference/core/filters.rst +++ b/docs/reference/core/filters.rst @@ -25,10 +25,12 @@ Date Filters .. autoclass:: BeforeAfterFilter :members: + :inherited-members: :show-inheritance: .. autoclass:: OnBeforeAfterFilter :members: + :inherited-members: :show-inheritance: Collection Filters @@ -84,10 +86,12 @@ Search .. autoclass:: SearchFilter :members: + :inherited-members: :show-inheritance: .. autoclass:: NotInSearchFilter :members: + :inherited-members: :show-inheritance: Type Aliases diff --git a/sqlspec/__init__.py b/sqlspec/__init__.py index 88c6341ef..e38c31a16 100644 --- a/sqlspec/__init__.py +++ b/sqlspec/__init__.py @@ -16,9 +16,6 @@ from typing import TYPE_CHECKING, Any from sqlspec import adapters, base, builder, core, driver, exceptions, extensions, loader, migrations, typing, utils - -if TYPE_CHECKING: - from sqlspec import dialects from sqlspec.__metadata__ import __version__ from sqlspec.base import SQLSpec from sqlspec.builder import ( @@ -86,6 +83,9 @@ from sqlspec.typing import ConnectionT, PoolT, SchemaT, StatementParameters, SupportedSchemaModel from sqlspec.utils.logging import suppress_erroneous_sqlglot_log_messages +if TYPE_CHECKING: + from sqlspec import dialects + __all__ = ( "SQL", "ArrowResult", diff --git a/sqlspec/adapters/adbc/config.py b/sqlspec/adapters/adbc/config.py index 8f15c63f7..b180ebb11 100644 --- a/sqlspec/adapters/adbc/config.py +++ b/sqlspec/adapters/adbc/config.py @@ -13,6 +13,7 @@ get_statement_config, is_postgres_dialect, resolve_dialect_from_config, + resolve_dialect_name, resolve_driver_connect_func, resolve_postgres_extension_state, resolve_runtime_statement_config, @@ -194,7 +195,7 @@ def __init__( self, *, connection_config: "AdbcConnectionParams | dict[str, Any] | None" = None, - connection_instance: "Any" = None, + connection_instance: "AdbcConnection | None" = None, migration_config: "dict[str, Any] | None" = None, statement_config: StatementConfig | None = None, driver_features: "AdbcDriverFeatures | dict[str, Any] | None" = None, @@ -256,13 +257,15 @@ def create_connection(self) -> AdbcConnection: """ try: - connection = resolve_driver_connect_func( - self.connection_config.get("driver_name"), self.connection_config.get("uri") - )(**build_connection_config(self.connection_config)) + driver_name = cast("str | None", self.connection_config.get("driver_name")) + uri = cast("str | None", self.connection_config.get("uri")) + connection = resolve_driver_connect_func(driver_name, uri)( + **build_connection_config(self.connection_config) + ) return cast("AdbcConnection", connection) except Exception as e: - driver_name = self.connection_config.get("driver_name", "Unknown") - msg = f"Could not configure connection using driver '{driver_name}'. Error: {e}" + err_driver_name = self.connection_config.get("driver_name", "Unknown") + msg = f"Could not configure connection using driver '{err_driver_name}'. Error: {e}" raise ImproperConfigurationError(msg) from e def _update_dialect_for_extensions(self) -> None: @@ -271,7 +274,7 @@ def _update_dialect_for_extensions(self) -> None: Priority: paradedb > pgvector > postgres (default). Only switches when current dialect is ``postgres``. """ - current_dialect = getattr(self.statement_config, "dialect", "postgres") + current_dialect = self.statement_config.dialect or "postgres" if current_dialect != "postgres": return @@ -289,7 +292,7 @@ def _detect_extensions_if_needed(self) -> None: if self._pgvector_available is not None: return - dialect = getattr(self.statement_config, "dialect", "") + dialect = resolve_dialect_name(self.statement_config.dialect) if not is_postgres_dialect(dialect): self._pgvector_available = False self._paradedb_available = False diff --git a/sqlspec/adapters/aiomysql/core.py b/sqlspec/adapters/aiomysql/core.py index 00b195be0..fe0fc2ea9 100644 --- a/sqlspec/adapters/aiomysql/core.py +++ b/sqlspec/adapters/aiomysql/core.py @@ -4,8 +4,6 @@ from collections.abc import Callable, Sized from typing import TYPE_CHECKING, Any -from aiomysql import SSCursor - from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile from sqlspec.driver import rows_to_dicts from sqlspec.exceptions import ( @@ -24,6 +22,7 @@ TransactionError, UniqueViolationError, ) +from sqlspec.protocols import HasSqlStateProtocol, HasTypeCodeProtocol from sqlspec.utils.serializers import from_json, to_json from sqlspec.utils.text import quote_backtick_identifier, split_qualified_identifier from sqlspec.utils.type_converters import build_uuid_coercions @@ -189,6 +188,8 @@ def __init__(self, driver: Any, sql: str, parameters: Any, chunk_size: int) -> N self._column_names: list[str] | None = None async def start(self) -> None: + from aiomysql import SSCursor + handler = self._driver.handle_database_exceptions() async with handler: cursor = await self._driver.connection.cursor(SSCursor) @@ -324,13 +325,11 @@ def create_mapped_exception(error: Any, *, logger: Any | None = None) -> "SQLSpe Returns: True to suppress expected migration errors, or a SQLSpec exception """ - error_args = getattr(error, "args", ()) + error_args = error.args error_code = error_args[0] if error_args and isinstance(error_args[0], int) else None - sqlstate_attr = getattr(error, "sqlstate", None) - sqlstate = sqlstate_attr if isinstance(sqlstate_attr, str) else None + sqlstate = error.sqlstate if isinstance(error, HasSqlStateProtocol) else None sqlstate_prefix = sqlstate[:2] if isinstance(sqlstate, str) and sqlstate else None - # Migration-specific errors to suppress if error_code in _MYSQL_MIGRATION_ERROR_CODES: if logger is not None: logger.warning("aiomysql MySQL expected migration error (ignoring): %s", error) @@ -402,7 +401,7 @@ def detect_json_columns_from_description( if isinstance(column, (tuple, list)): type_code = column[1] if len(column) > 1 else None else: - type_code = getattr(column, "type_code", None) + type_code = column.type_code if isinstance(column, HasTypeCodeProtocol) else None if type_code in json_type_codes: append(index) return json_indexes diff --git a/sqlspec/adapters/arrow_odbc/config.py b/sqlspec/adapters/arrow_odbc/config.py index 05c870106..036286322 100644 --- a/sqlspec/adapters/arrow_odbc/config.py +++ b/sqlspec/adapters/arrow_odbc/config.py @@ -11,6 +11,7 @@ from sqlspec.driver._sync import SyncPoolConnectionContext, SyncPoolSessionFactory from sqlspec.exceptions import ImproperConfigurationError from sqlspec.extensions.events import EventRuntimeHints +from sqlspec.protocols import SupportsCloseProtocol from sqlspec.utils.config_tools import normalize_connection_config if TYPE_CHECKING: @@ -124,7 +125,7 @@ def __init__( self, *, connection_config: "ArrowOdbcConnectionParams | dict[str, Any] | None" = None, - connection_instance: "Any" = None, + connection_instance: "ArrowOdbcConnection | None" = None, migration_config: "dict[str, Any] | None" = None, statement_config: "StatementConfig | None" = None, driver_features: "ArrowOdbcDriverFeatures | dict[str, Any] | None" = None, @@ -204,6 +205,5 @@ def get_event_runtime_hints(self) -> "EventRuntimeHints": def _close_arrow_odbc_connection(connection: "ArrowOdbcConnection") -> None: """Close connection objects from compatible wrappers when they expose close().""" - close = getattr(connection, "close", None) - if close is not None: - close() + if isinstance(connection, SupportsCloseProtocol): + connection.close() diff --git a/sqlspec/adapters/asyncmy/core.py b/sqlspec/adapters/asyncmy/core.py index 290f0a662..b27eff2b2 100644 --- a/sqlspec/adapters/asyncmy/core.py +++ b/sqlspec/adapters/asyncmy/core.py @@ -4,8 +4,6 @@ from collections.abc import Callable, Sized from typing import TYPE_CHECKING, Any -from asyncmy.cursors import SSCursor - from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile from sqlspec.driver import rows_to_dicts from sqlspec.exceptions import ( @@ -24,6 +22,7 @@ TransactionError, UniqueViolationError, ) +from sqlspec.protocols import HasSqlStateProtocol, HasTypeCodeProtocol from sqlspec.utils.serializers import from_json, to_json from sqlspec.utils.text import quote_backtick_identifier, split_qualified_identifier from sqlspec.utils.type_converters import build_uuid_coercions @@ -161,6 +160,8 @@ def __init__(self, driver: Any, sql: str, parameters: Any, chunk_size: int) -> N self._column_names: list[str] | None = None async def start(self) -> None: + from asyncmy.cursors import SSCursor + handler = self._driver.handle_database_exceptions() async with handler: cursor = self._driver.connection.cursor(SSCursor) @@ -296,13 +297,11 @@ def create_mapped_exception(error: Any, *, logger: Any | None = None) -> "SQLSpe Returns: True to suppress expected migration errors, or a SQLSpec exception """ - error_args = getattr(error, "args", ()) + error_args = error.args error_code = error_args[0] if error_args and isinstance(error_args[0], int) else None - sqlstate_attr = getattr(error, "sqlstate", None) - sqlstate = sqlstate_attr if isinstance(sqlstate_attr, str) else None + sqlstate = error.sqlstate if isinstance(error, HasSqlStateProtocol) else None sqlstate_prefix = sqlstate[:2] if isinstance(sqlstate, str) and sqlstate else None - # Migration-specific errors to suppress if error_code in _MYSQL_MIGRATION_ERROR_CODES: if logger is not None: logger.warning("AsyncMy MySQL expected migration error (ignoring): %s", error) @@ -374,7 +373,7 @@ def detect_json_columns_from_description( if isinstance(column, (tuple, list)): type_code = column[1] if len(column) > 1 else None else: - type_code = getattr(column, "type_code", None) + type_code = column.type_code if isinstance(column, HasTypeCodeProtocol) else None if type_code in json_type_codes: append(index) return json_indexes diff --git a/sqlspec/adapters/asyncpg/_typing.py b/sqlspec/adapters/asyncpg/_typing.py index 443297842..cb8c5e64b 100644 --- a/sqlspec/adapters/asyncpg/_typing.py +++ b/sqlspec/adapters/asyncpg/_typing.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any -from asyncpg import Pool +from asyncpg import Pool, PostgresError from asyncpg.pool import PoolConnectionProxy from asyncpg.prepared_stmt import PreparedStatement @@ -22,14 +22,23 @@ AsyncpgConnection: TypeAlias = Connection[Record] | PoolConnectionProxy[Record] AsyncpgPool: TypeAlias = Pool[Record] + AsyncpgPostgresError: TypeAlias = PostgresError AsyncpgPreparedStatement: TypeAlias = PreparedStatement[Record] if not TYPE_CHECKING: AsyncpgConnection = PoolConnectionProxy AsyncpgPool = Pool + AsyncpgPostgresError = PostgresError AsyncpgPreparedStatement = PreparedStatement -__all__ = ("AsyncpgConnection", "AsyncpgCursor", "AsyncpgPool", "AsyncpgPreparedStatement", "AsyncpgSessionContext") +__all__ = ( + "AsyncpgConnection", + "AsyncpgCursor", + "AsyncpgPool", + "AsyncpgPostgresError", + "AsyncpgPreparedStatement", + "AsyncpgSessionContext", +) class AsyncpgCursor: diff --git a/sqlspec/adapters/asyncpg/driver.py b/sqlspec/adapters/asyncpg/driver.py index e5f952356..58f332108 100644 --- a/sqlspec/adapters/asyncpg/driver.py +++ b/sqlspec/adapters/asyncpg/driver.py @@ -5,9 +5,7 @@ from io import BytesIO from typing import TYPE_CHECKING, Any, Final, cast -import asyncpg - -from sqlspec.adapters.asyncpg._typing import AsyncpgCursor, AsyncpgSessionContext +from sqlspec.adapters.asyncpg._typing import AsyncpgCursor, AsyncpgPostgresError, AsyncpgSessionContext from sqlspec.adapters.asyncpg.core import ( PREPARED_STATEMENT_CACHE_SIZE, AsyncpgStreamSource, @@ -77,7 +75,7 @@ class AsyncpgExceptionHandler(BaseAsyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: _ = exc_type - if isinstance(exc_val, asyncpg.PostgresError) or has_sqlstate(exc_val): + if isinstance(exc_val, AsyncpgPostgresError) or has_sqlstate(exc_val): self.pending_exception = create_mapped_exception(exc_val) return True return False @@ -217,7 +215,7 @@ async def begin(self) -> None: """Begin a database transaction.""" try: await self.connection.execute("BEGIN") - except asyncpg.PostgresError as e: + except AsyncpgPostgresError as e: msg = f"Failed to begin async transaction: {e}" raise SQLSpecError(msg) from e @@ -225,7 +223,7 @@ async def commit(self) -> None: """Commit the current transaction.""" try: await self.connection.execute("COMMIT") - except asyncpg.PostgresError as e: + except AsyncpgPostgresError as e: msg = f"Failed to commit async transaction: {e}" raise SQLSpecError(msg) from e @@ -233,7 +231,7 @@ async def rollback(self) -> None: """Rollback the current transaction.""" try: await self.connection.execute("ROLLBACK") - except asyncpg.PostgresError as e: + except AsyncpgPostgresError as e: msg = f"Failed to rollback async transaction: {e}" raise SQLSpecError(msg) from e @@ -414,13 +412,12 @@ async def load_from_arrow( telemetry: "StorageTelemetry | None" = None, ) -> "StorageBridgeJob": """Load Arrow data into a PostgreSQL table via COPY.""" - self._require_capability("arrow_import_enabled") arrow_table = self._coerce_arrow_table(source) if overwrite: try: await self.connection.execute(f"TRUNCATE TABLE {table}") - except asyncpg.PostgresError as exc: + except AsyncpgPostgresError as exc: msg = f"Failed to truncate table '{table}': {exc}" raise SQLSpecError(msg) from exc columns, records = self._arrow_table_to_rows(arrow_table) @@ -504,7 +501,7 @@ async def _handle_copy_operation(self, cursor: "AsyncpgConnection", statement: " execution_args = statement.statement_config.execution_args metadata: dict[str, Any] = dict(execution_args) if execution_args else {} - if getattr(statement, "is_processed", False): + if statement.is_processed: sql_text = statement.get_processed_state().compiled_sql else: sql_text, _ = self._get_compiled_sql(statement, statement.statement_config) diff --git a/sqlspec/adapters/bigquery/_typing.py b/sqlspec/adapters/bigquery/_typing.py index 54db18cdc..6f56e6943 100644 --- a/sqlspec/adapters/bigquery/_typing.py +++ b/sqlspec/adapters/bigquery/_typing.py @@ -8,6 +8,9 @@ from typing import TYPE_CHECKING, Any from google.cloud.bigquery import ArrayQueryParameter, Client, QueryJob, ScalarQueryParameter +from google.cloud.exceptions import GoogleCloudError + +from sqlspec.typing import import_optional if TYPE_CHECKING: from collections.abc import Callable @@ -19,12 +22,24 @@ BigQueryConnection: TypeAlias = Client BigQueryParam: TypeAlias = ArrayQueryParameter | ScalarQueryParameter + BigQueryStorageWriteModule: Any + BigQueryStorageWriteTypes: Any if not TYPE_CHECKING: BigQueryConnection = Client BigQueryParam = ArrayQueryParameter | ScalarQueryParameter - -__all__ = ("BigQueryConnection", "BigQueryCursor", "BigQueryParam", "BigQuerySessionContext") + BigQueryStorageWriteModule = import_optional("google.cloud.bigquery_storage_v1") + BigQueryStorageWriteTypes = import_optional("google.cloud.bigquery_storage_v1.types") + +__all__ = ( + "BigQueryConnection", + "BigQueryCursor", + "BigQueryParam", + "BigQuerySessionContext", + "BigQueryStorageWriteModule", + "BigQueryStorageWriteTypes", + "GoogleCloudError", +) class BigQueryCursor: diff --git a/sqlspec/adapters/bigquery/core.py b/sqlspec/adapters/bigquery/core.py index 3a13064a3..9fc6b694a 100644 --- a/sqlspec/adapters/bigquery/core.py +++ b/sqlspec/adapters/bigquery/core.py @@ -9,10 +9,6 @@ from urllib.parse import urlparse import sqlglot -from google.api_core.retry import Retry -from google.cloud.bigquery import LoadJobConfig, QueryJob, QueryJobConfig -from google.cloud.bigquery.retry import DEFAULT_RETRY -from google.cloud.exceptions import GoogleCloudError from sqlglot import exp from sqlspec.core import ( @@ -44,6 +40,9 @@ from collections.abc import Callable, Iterable, Iterator, Mapping from typing import Literal + from google.api_core.retry import Retry + from google.cloud.bigquery import LoadJobConfig, QueryJob, QueryJobConfig + from sqlspec.adapters.bigquery._typing import BigQueryConnection, BigQueryParam from sqlspec.driver._common import SyncExceptionHandler from sqlspec.storage import StorageTelemetry @@ -159,7 +158,8 @@ def _has_synthetic_positional_keys(parameters: "list[dict[str, Any]]") -> bool: def _uses_local_bigquery_endpoint(connection: "BigQueryConnection") -> bool: """Return True when a BigQuery client points at a local emulator endpoint.""" - api_base_url = getattr(getattr(connection, "_connection", None), "API_BASE_URL", None) + bq_conn = cast("object", getattr(connection, "_connection", None)) + api_base_url = cast("str | None", getattr(bq_conn, "API_BASE_URL", None)) if bq_conn is not None else None if not isinstance(api_base_url, str): return False try: @@ -459,6 +459,8 @@ def _inline_bigquery_literals( def _should_retry_bigquery_job(exception: Exception) -> bool: """Return True when a BigQuery job exception is safe to retry.""" + from google.cloud.exceptions import GoogleCloudError + if not isinstance(exception, GoogleCloudError): return False @@ -484,6 +486,8 @@ def _should_retry_bigquery_job(exception: Exception) -> bool: def build_retry(deadline: float) -> "Retry": """Build retry policy for job restarts based on error reason codes.""" + from google.api_core.retry import Retry + return Retry(predicate=_should_retry_bigquery_job, deadline=deadline) @@ -517,13 +521,13 @@ def build_retry(deadline: float) -> "Retry": ) -def copy_job_config(source_config: QueryJobConfig, target_config: QueryJobConfig) -> None: +def copy_job_config(source_config: "QueryJobConfig", target_config: "QueryJobConfig") -> None: """Copy known job config fields from source to target.""" for attr in _COPY_JOB_FIELDS: _copy_job_config_field(source_config, target_config, attr) -def _copy_job_config_field(source_config: QueryJobConfig, target_config: QueryJobConfig, attr: str) -> None: +def _copy_job_config_field(source_config: "QueryJobConfig", target_config: "QueryJobConfig", attr: str) -> None: try: value = getattr(source_config, attr) except (AttributeError, TypeError): @@ -537,17 +541,17 @@ def run_query_job( sql: str, parameters: Any, *, - default_job_config: QueryJobConfig | None, - job_config: QueryJobConfig | None, + default_job_config: "QueryJobConfig | None", + job_config: "QueryJobConfig | None", json_serializer: "Callable[[Any], str]", - retry: Retry | None = None, + retry: "Retry | None" = None, timeout: float | None = None, - job_retry: Retry | None = None, + job_retry: "Retry | None" = None, api_method: str | None = None, timestamp_precision: Any | None = None, job_id: str | None = None, job_id_prefix: str | None = None, -) -> QueryJob: +) -> "QueryJob": """Execute a BigQuery query job with merged configuration. Args: @@ -573,6 +577,8 @@ def run_query_job( Returns: QueryJob object representing the executed job. """ + from google.cloud.bigquery import QueryJobConfig + final_job_config = QueryJobConfig() if default_job_config: copy_job_config(default_job_config, final_job_config) @@ -602,15 +608,17 @@ def _run_query_and_wait( sql: str, parameters: Any, *, - default_job_config: QueryJobConfig | None, + default_job_config: "QueryJobConfig | None", json_serializer: "Callable[[Any], str]", - retry: Retry | None = None, + retry: "Retry | None" = None, wait_timeout: float | None = None, - job_retry: Retry | None = None, + job_retry: "Retry | None" = None, page_size: int | None = None, max_results: int | None = None, ) -> Any: """Execute a BigQuery query via query_and_wait and return the row iterator.""" + from google.cloud.bigquery import QueryJobConfig + final_job_config = QueryJobConfig() if default_job_config: copy_job_config(default_job_config, final_job_config) @@ -632,6 +640,8 @@ def _run_query_and_wait( def build_load_job_config(file_format: "BigQueryLoadFormat", overwrite: bool) -> "LoadJobConfig": + from google.cloud.bigquery import LoadJobConfig + job_config = LoadJobConfig() job_config.source_format = _map_bigquery_source_format(file_format) job_config.write_disposition = "WRITE_TRUNCATE" if overwrite else "WRITE_APPEND" @@ -701,7 +711,7 @@ def _append_request_size(request: Any) -> int: return size -def build_load_job_telemetry(job: QueryJob, table: str, *, format_label: str) -> "StorageTelemetry": +def build_load_job_telemetry(job: "QueryJob", table: str, *, format_label: str) -> "StorageTelemetry": try: properties = cast("Any", job)._properties except AttributeError: @@ -824,6 +834,8 @@ def __init__( self._pages: Iterator[Iterable[_BigQueryRow]] | None = None def start(self) -> None: + from google.cloud.bigquery.retry import DEFAULT_RETRY + handler = self._driver.handle_database_exceptions() with handler: page_size = None if _uses_local_bigquery_endpoint(self._driver.connection) else self._chunk_size @@ -1046,6 +1058,14 @@ def create_mapped_exception(error: Any) -> SQLSpecError: 2. Message pattern matching for specific errors 3. Default SQLSpecError fallback + Mapped Statuses: + * UniqueViolationError: HTTP 409 (Conflict) or "already exists" in message + * NotFoundError: HTTP 404 (Not Found) or "not found" in message + * QueryTimeoutError: "timeout", "deadline exceeded", or "cancelled" in message + * SQLParsingError / DataError / SQLSpecError: HTTP 400 (Bad Request) + * PermissionDeniedError: HTTP 403 (Forbidden) or "access denied" / "permission denied" in message + * OperationalError: HTTP 500+ (Server error) + Args: error: The BigQuery exception to map @@ -1058,19 +1078,15 @@ def create_mapped_exception(error: Any) -> SQLSpecError: status_code = None error_msg = str(error).lower() - # Resource conflict - unique/already exists if status_code == HTTP_CONFLICT or "already exists" in error_msg: return _create_bigquery_error(error, status_code, UniqueViolationError, "resource already exists") - # Resource not found if status_code == HTTP_NOT_FOUND or "not found" in error_msg: return _create_bigquery_error(error, status_code, NotFoundError, "resource not found") - # Query timeout/cancellation if "timeout" in error_msg or "deadline exceeded" in error_msg or "cancelled" in error_msg: return _create_bigquery_error(error, status_code, QueryTimeoutError, "query timeout or cancelled") - # Bad request - parse message for details if status_code == HTTP_BAD_REQUEST: if "syntax" in error_msg or "invalid query" in error_msg: return _create_bigquery_error(error, status_code, SQLParsingError, "query syntax error") @@ -1078,11 +1094,9 @@ def create_mapped_exception(error: Any) -> SQLSpecError: return _create_bigquery_error(error, status_code, DataError, "data error") return _create_bigquery_error(error, status_code, SQLSpecError, "error") - # Permission denied - use PermissionDeniedError instead of DatabaseConnectionError if status_code == HTTP_FORBIDDEN or "access denied" in error_msg or "permission denied" in error_msg: return _create_bigquery_error(error, status_code, PermissionDeniedError, "permission denied") - # Server errors if status_code and status_code >= HTTP_SERVER_ERROR: return _create_bigquery_error(error, status_code, OperationalError, "operational error") diff --git a/sqlspec/adapters/bigquery/driver.py b/sqlspec/adapters/bigquery/driver.py index 14e49c873..4ebfb5732 100644 --- a/sqlspec/adapters/bigquery/driver.py +++ b/sqlspec/adapters/bigquery/driver.py @@ -11,9 +11,15 @@ from typing import TYPE_CHECKING, Any, cast from google.cloud.bigquery.retry import POLLING_DEFAULT_VALUE -from google.cloud.exceptions import GoogleCloudError -from sqlspec.adapters.bigquery._typing import BigQueryConnection, BigQueryCursor, BigQuerySessionContext +from sqlspec.adapters.bigquery._typing import ( + BigQueryConnection, + BigQueryCursor, + BigQuerySessionContext, + BigQueryStorageWriteModule, + BigQueryStorageWriteTypes, + GoogleCloudError, +) from sqlspec.adapters.bigquery.core import ( DEFAULT_REQUEST_TIMEOUT, BigQueryStreamSource, @@ -636,13 +642,15 @@ def load_from_arrow( def _load_arrow_via_storage_write_api(self, table: str, arrow_table: "Any") -> "StorageTelemetry": """Ingest an Arrow table via a BigQuery PENDING write stream using native arrow_rows.""" - from google.cloud import bigquery_storage_v1 - from google.cloud.bigquery_storage_v1 import types + if BigQueryStorageWriteModule is None or BigQueryStorageWriteTypes is None: + msg = "google-cloud-bigquery-storage is required for BigQuery Storage Write API ingestion" + raise ImportError(msg) + types = BigQueryStorageWriteTypes project, dataset, table_name = _resolve_storage_write_table_path(table, self.connection.project) credentials = getattr(self.connection, "_credentials", None) - client = bigquery_storage_v1.BigQueryWriteClient(credentials=credentials) # type: ignore[no-untyped-call] + client = BigQueryStorageWriteModule.BigQueryWriteClient(credentials=credentials) parent = f"projects/{project}/datasets/{dataset}/tables/{table_name}" write_stream = client.create_write_stream( parent=parent, write_stream=types.WriteStream(type_=types.WriteStream.Type.PENDING) diff --git a/sqlspec/adapters/cockroach_asyncpg/_typing.py b/sqlspec/adapters/cockroach_asyncpg/_typing.py index 8a1d8345e..2e96891dd 100644 --- a/sqlspec/adapters/cockroach_asyncpg/_typing.py +++ b/sqlspec/adapters/cockroach_asyncpg/_typing.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from asyncpg import Pool +from asyncpg import Pool, PostgresError from asyncpg.pool import PoolConnectionProxy if TYPE_CHECKING: @@ -16,13 +16,20 @@ from sqlspec.core import StatementConfig CockroachAsyncpgConnection: TypeAlias = Connection[Record] | PoolConnectionProxy[Record] + CockroachAsyncpgPostgresError: TypeAlias = PostgresError CockroachAsyncpgPool: TypeAlias = Pool[Record] if not TYPE_CHECKING: CockroachAsyncpgConnection = PoolConnectionProxy + CockroachAsyncpgPostgresError = PostgresError CockroachAsyncpgPool = Pool -__all__ = ("CockroachAsyncpgConnection", "CockroachAsyncpgPool", "CockroachAsyncpgSessionContext") +__all__ = ( + "CockroachAsyncpgConnection", + "CockroachAsyncpgPool", + "CockroachAsyncpgPostgresError", + "CockroachAsyncpgSessionContext", +) class CockroachAsyncpgSessionContext: diff --git a/sqlspec/adapters/cockroach_asyncpg/driver.py b/sqlspec/adapters/cockroach_asyncpg/driver.py index e52706f00..b8000a464 100644 --- a/sqlspec/adapters/cockroach_asyncpg/driver.py +++ b/sqlspec/adapters/cockroach_asyncpg/driver.py @@ -4,11 +4,9 @@ import contextlib from typing import TYPE_CHECKING, Any, cast -import asyncpg - from sqlspec.adapters.asyncpg.core import create_mapped_exception, driver_profile from sqlspec.adapters.asyncpg.driver import AsyncpgDriver -from sqlspec.adapters.cockroach_asyncpg._typing import CockroachAsyncpgSessionContext +from sqlspec.adapters.cockroach_asyncpg._typing import CockroachAsyncpgPostgresError, CockroachAsyncpgSessionContext from sqlspec.adapters.cockroach_asyncpg.core import ( CockroachAsyncpgRetryConfig, calculate_backoff_seconds, @@ -40,7 +38,7 @@ class CockroachAsyncpgExceptionHandler(BaseAsyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: _ = exc_type - if isinstance(exc_val, asyncpg.PostgresError) or has_sqlstate(exc_val): + if isinstance(exc_val, CockroachAsyncpgPostgresError) or has_sqlstate(exc_val): if has_sqlstate(exc_val) and str(exc_val.sqlstate) == "40001": self.pending_exception = SerializationConflictError(str(exc_val)) return True diff --git a/sqlspec/adapters/duckdb/_typing.py b/sqlspec/adapters/duckdb/_typing.py index 33d84d09f..92ca248cb 100644 --- a/sqlspec/adapters/duckdb/_typing.py +++ b/sqlspec/adapters/duckdb/_typing.py @@ -45,7 +45,7 @@ def __enter__(self) -> "DuckDBConnection": return self.connection def __exit__(self, *_: Any) -> None: - pass # Connection lifecycle managed by pool/session + """Connection lifecycle managed by pool/session.""" class DuckDBSessionContext: @@ -71,8 +71,8 @@ class DuckDBSessionContext: def __init__( self, - acquire_connection: "Callable[[], Any]", - release_connection: "Callable[[Any], Any]", + acquire_connection: "Callable[[], DuckDBConnection]", + release_connection: "Callable[[DuckDBConnection], None]", statement_config: "StatementConfig", driver_features: "dict[str, Any]", prepare_driver: "Callable[[DuckDBDriver], DuckDBDriver]", @@ -82,7 +82,7 @@ def __init__( self._statement_config = statement_config self._driver_features = driver_features self._prepare_driver = prepare_driver - self._connection: Any = None + self._connection: DuckDBConnection | None = None self._driver: DuckDBDriver | None = None def __enter__(self) -> "DuckDBDriver": diff --git a/sqlspec/adapters/duckdb/config.py b/sqlspec/adapters/duckdb/config.py index b656aba0e..729141477 100644 --- a/sqlspec/adapters/duckdb/config.py +++ b/sqlspec/adapters/duckdb/config.py @@ -270,7 +270,6 @@ def __init__( extension_flags[key] = connection_config.pop(key) features: dict[str, Any] = dict(driver_features) if driver_features else {} - # Extract and store callback for pool - pool is source of truth for connection initialization self._user_connection_hook = cast( "Callable[[DuckDBConnection], DuckDBConnection | None] | None", features.pop("on_connection_create", None) ) @@ -303,9 +302,9 @@ def _create_pool(self) -> DuckDBConnectionPool: """Create connection pool from configuration.""" connection_config = build_connection_config(self.connection_config) - extensions = self.driver_features.get("extensions", None) - secrets = self.driver_features.get("secrets", None) - extension_flags = self.driver_features.get("extension_flags", None) + extensions = cast("list[dict[str, Any]] | None", self.driver_features.get("extensions", None)) + secrets = cast("list[dict[str, Any]] | None", self.driver_features.get("secrets", None)) + extension_flags = cast("dict[str, Any] | None", self.driver_features.get("extension_flags", None)) extensions_dicts = [dict(ext) for ext in extensions] if extensions else None secrets_dicts = [dict(secret) for secret in secrets] if secrets else None extension_flags_dict = dict(extension_flags) if extension_flags else None diff --git a/sqlspec/adapters/duckdb/data_dictionary.py b/sqlspec/adapters/duckdb/data_dictionary.py index 7a1dc10cb..bfb0678d1 100644 --- a/sqlspec/adapters/duckdb/data_dictionary.py +++ b/sqlspec/adapters/duckdb/data_dictionary.py @@ -32,10 +32,8 @@ def get_version(self, driver: "DuckDBDriver") -> "VersionInfo | None": DuckDB version information or None if detection fails. """ driver_id = id(driver) - # Inline cache check to avoid cross-module method call that causes mypyc segfault if driver_id in self._version_fetch_attempted: return self._version_cache.get(driver_id) - # Not cached, fetch from database version_value = driver.select_value_or_none(self.get_query("version")) if not version_value: diff --git a/sqlspec/adapters/duckdb/driver.py b/sqlspec/adapters/duckdb/driver.py index 821783681..e25927b38 100644 --- a/sqlspec/adapters/duckdb/driver.py +++ b/sqlspec/adapters/duckdb/driver.py @@ -281,19 +281,15 @@ def select_to_arrow( """ ensure_pyarrow() - # Prepare statement config = statement_config or self.statement_config prepared_statement = self.prepare_statement(statement, parameters, statement_config=config, kwargs=kwargs) exc_handler = self.handle_database_exceptions() arrow_result: ArrowResult | None = None - # Execute query and get native Arrow with self.with_cursor(self.connection) as cursor, exc_handler: - # Get compiled SQL and parameters sql, driver_params = self._get_compiled_sql(prepared_statement, config) - # Execute query cursor.execute(sql, driver_params or ()) if return_format in {"reader", "batches"}: @@ -308,7 +304,6 @@ def select_to_arrow( arrow_schema=arrow_schema, ) - # DuckDB native Arrow (zero-copy!) arrow_table = cursor.to_arrow_table() arrow_result = build_arrow_result_from_table( diff --git a/sqlspec/adapters/duckdb/pool.py b/sqlspec/adapters/duckdb/pool.py index 102578218..a9cde49c2 100644 --- a/sqlspec/adapters/duckdb/pool.py +++ b/sqlspec/adapters/duckdb/pool.py @@ -93,8 +93,6 @@ def __init__( self._thread_local = threading.local() self._lock = threading.RLock() self._pool_id = str(uuid.uuid4())[:8] - # Track if this pool uses an in-memory database - # In-memory databases require connections to stay alive to preserve data database = connection_config.get("database", "") self._is_memory_db = database.startswith(":memory:") or database == "" @@ -159,8 +157,6 @@ def _create_connection(self) -> DuckDBConnection: _create_secret(connection, secret_config) if self._on_connection_create: - # Let a failing user hook surface its real error instead of silently returning a - # half-configured connection (mirrors the sqlite/aiosqlite pools). self._on_connection_create(connection) return connection @@ -335,8 +331,6 @@ def get_connection(self) -> "Generator[DuckDBConnection, None, None]": self._close_thread_connection() raise else: - # Only close connection for file-based databases to release file locks - # In-memory databases need connections to stay alive to preserve data if not self._is_memory_db: self._close_thread_connection() diff --git a/sqlspec/adapters/mssql_python/_typing.py b/sqlspec/adapters/mssql_python/_typing.py index 457b668f3..a79a02c0d 100644 --- a/sqlspec/adapters/mssql_python/_typing.py +++ b/sqlspec/adapters/mssql_python/_typing.py @@ -10,7 +10,7 @@ MSSQL_PYTHON_MODULE: Any = _mssql_python if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Awaitable, Callable from types import TracebackType from typing import TypeAlias @@ -86,8 +86,8 @@ class MssqlPythonSessionContext: def __init__( self, - acquire_connection: "Callable[[], Any]", - release_connection: "Callable[[Any], Any]", + acquire_connection: "Callable[[], MssqlPythonConnection]", + release_connection: "Callable[[MssqlPythonConnection], None]", statement_config: "StatementConfig", driver_features: "dict[str, Any]", prepare_driver: "Callable[[MssqlPythonDriver], MssqlPythonDriver]", @@ -97,7 +97,7 @@ def __init__( self._statement_config = statement_config self._driver_features = driver_features self._prepare_driver = prepare_driver - self._connection: Any = None + self._connection: MssqlPythonConnection | None = None self._driver: MssqlPythonDriver | None = None def __enter__(self) -> "MssqlPythonDriver": @@ -133,8 +133,8 @@ class MssqlPythonAsyncSessionContext: def __init__( self, - acquire_connection: "Callable[[], Any]", - release_connection: "Callable[[Any], Any]", + acquire_connection: "Callable[[], Awaitable[MssqlPythonConnection]]", + release_connection: "Callable[[MssqlPythonConnection], Awaitable[None]]", statement_config: "StatementConfig", driver_features: "dict[str, Any]", prepare_driver: "Callable[[MssqlPythonAsyncDriver], MssqlPythonAsyncDriver]", @@ -144,7 +144,7 @@ def __init__( self._statement_config = statement_config self._driver_features = driver_features self._prepare_driver = prepare_driver - self._connection: Any = None + self._connection: MssqlPythonConnection | None = None self._driver: MssqlPythonAsyncDriver | None = None async def __aenter__(self) -> "MssqlPythonAsyncDriver": diff --git a/sqlspec/adapters/mssql_python/data_dictionary.py b/sqlspec/adapters/mssql_python/data_dictionary.py index 4f5cb74f0..106945458 100644 --- a/sqlspec/adapters/mssql_python/data_dictionary.py +++ b/sqlspec/adapters/mssql_python/data_dictionary.py @@ -26,6 +26,7 @@ from sqlspec.utils.logging import get_logger if TYPE_CHECKING: + from sqlspec.adapters.mssql_python.driver import MssqlPythonAsyncDriver, MssqlPythonDriver from sqlspec.data_dictionary._types import DialectConfig __all__ = ("MssqlPythonAsyncDataDictionary", "MssqlPythonSyncDataDictionary", "MssqlVersionInfo") @@ -120,7 +121,7 @@ class MssqlPythonSyncDataDictionary(_MssqlDataDictionaryMixin, SyncDataDictionar def __init__(self) -> None: super().__init__() - def get_version(self, driver: Any) -> MssqlVersionInfo | None: + def get_version(self, driver: "MssqlPythonDriver") -> MssqlVersionInfo | None: """Get SQL Server version information.""" driver_id = id(driver) if driver_id in self._version_fetch_attempted: @@ -147,7 +148,7 @@ def get_version(self, driver: Any) -> MssqlVersionInfo | None: self.cache_version(driver_id, version_info) return version_info - def get_feature_flag(self, driver: Any, feature: str) -> bool: + def get_feature_flag(self, driver: "MssqlPythonDriver", feature: str) -> bool: """Check whether SQL Server supports a feature.""" version_info = self.get_version(driver) return resolve_mssql_feature_flag( @@ -158,11 +159,11 @@ def get_feature_flag(self, driver: Any, feature: str) -> bool: version_info=version_info, ) - def get_optimal_type(self, driver: Any, type_category: str) -> str: + def get_optimal_type(self, driver: "MssqlPythonDriver", type_category: str) -> str: """Get optimal SQL Server type for a category.""" return self._get_optimal_type_from_version(self.get_version(driver), type_category) - def get_tables(self, driver: Any, schema: str | None = None) -> list[TableMetadata]: + def get_tables(self, driver: "MssqlPythonDriver", 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") @@ -176,7 +177,9 @@ def get_tables(self, driver: Any, schema: str | None = None) -> list[TableMetada ) return merge_mssql_table_lists(ordered, all_rows) - def get_columns(self, driver: Any, table: str | None = None, schema: str | None = None) -> list[ColumnMetadata]: + def get_columns( + self, driver: "MssqlPythonDriver", table: str | None = None, schema: str | None = None + ) -> list[ColumnMetadata]: """Get column information for a table or schema.""" schema_name = self.resolve_schema(schema) if table is None: @@ -196,7 +199,9 @@ def get_columns(self, driver: Any, table: str | None = None, schema: str | None ), ) - def get_indexes(self, driver: Any, table: str | None = None, schema: str | None = None) -> list[IndexMetadata]: + def get_indexes( + self, driver: "MssqlPythonDriver", table: str | None = None, schema: str | None = None + ) -> list[IndexMetadata]: """Get index metadata for a table or schema.""" schema_name = self.resolve_schema(schema) if table is None: @@ -214,7 +219,7 @@ def get_indexes(self, driver: Any, table: str | None = None, schema: str | None ) def get_foreign_keys( - self, driver: Any, table: str | None = None, schema: str | None = None + self, driver: "MssqlPythonDriver", table: str | None = None, schema: str | None = None ) -> list[ForeignKeyMetadata]: """Get foreign key metadata.""" schema_name = self.resolve_schema(schema) @@ -247,7 +252,7 @@ class MssqlPythonAsyncDataDictionary(_MssqlDataDictionaryMixin, AsyncDataDiction def __init__(self) -> None: super().__init__() - async def get_version(self, driver: Any) -> MssqlVersionInfo | None: + async def get_version(self, driver: "MssqlPythonAsyncDriver") -> MssqlVersionInfo | None: """Get SQL Server version information.""" driver_id = id(driver) if driver_id in self._version_fetch_attempted: @@ -274,7 +279,7 @@ async def get_version(self, driver: Any) -> MssqlVersionInfo | None: self.cache_version(driver_id, version_info) return version_info - async def get_feature_flag(self, driver: Any, feature: str) -> bool: + async def get_feature_flag(self, driver: "MssqlPythonAsyncDriver", feature: str) -> bool: """Check whether SQL Server supports a feature.""" version_info = await self.get_version(driver) return resolve_mssql_feature_flag( @@ -285,11 +290,11 @@ async def get_feature_flag(self, driver: Any, feature: str) -> bool: version_info=version_info, ) - async def get_optimal_type(self, driver: Any, type_category: str) -> str: + async def get_optimal_type(self, driver: "MssqlPythonAsyncDriver", type_category: str) -> str: """Get optimal SQL Server type for a category.""" return self._get_optimal_type_from_version(await self.get_version(driver), type_category) - async def get_tables(self, driver: Any, schema: str | None = None) -> list[TableMetadata]: + async def get_tables(self, driver: "MssqlPythonAsyncDriver", 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") @@ -306,7 +311,7 @@ async def get_tables(self, driver: Any, schema: str | None = None) -> list[Table return merge_mssql_table_lists(ordered, all_rows) async def get_columns( - self, driver: Any, table: str | None = None, schema: str | None = None + self, driver: "MssqlPythonAsyncDriver", table: str | None = None, schema: str | None = None ) -> list[ColumnMetadata]: """Get column information for a table or schema.""" schema_name = self.resolve_schema(schema) @@ -330,7 +335,7 @@ async def get_columns( ) async def get_indexes( - self, driver: Any, table: str | None = None, schema: str | None = None + self, driver: "MssqlPythonAsyncDriver", table: str | None = None, schema: str | None = None ) -> list[IndexMetadata]: """Get index metadata for a table or schema.""" schema_name = self.resolve_schema(schema) @@ -351,7 +356,7 @@ async def get_indexes( ) async def get_foreign_keys( - self, driver: Any, table: str | None = None, schema: str | None = None + self, driver: "MssqlPythonAsyncDriver", table: str | None = None, schema: str | None = None ) -> list[ForeignKeyMetadata]: """Get foreign key metadata.""" schema_name = self.resolve_schema(schema) diff --git a/sqlspec/adapters/mssql_python/type_converter.py b/sqlspec/adapters/mssql_python/type_converter.py index c8769afb6..2bdad68b9 100644 --- a/sqlspec/adapters/mssql_python/type_converter.py +++ b/sqlspec/adapters/mssql_python/type_converter.py @@ -7,6 +7,8 @@ from sqlspec.utils.serializers import from_json, to_json if TYPE_CHECKING: + from collections.abc import Callable + import pyarrow as pa __all__ = ("MssqlPythonTypeConverter", "mssql_type_to_arrow") @@ -52,7 +54,9 @@ class MssqlPythonTypeConverter: __slots__ = ("_json_deserializer", "_json_serializer") - def __init__(self, json_serializer: "Any" = to_json, json_deserializer: "Any" = from_json) -> None: + def __init__( + self, json_serializer: "Callable[[Any], str]" = to_json, json_deserializer: "Callable[[str], Any]" = from_json + ) -> None: self._json_serializer = json_serializer self._json_deserializer = json_deserializer diff --git a/sqlspec/adapters/mysqlconnector/adk/store.py b/sqlspec/adapters/mysqlconnector/adk/store.py index 54b1408ff..c17a5c43a 100644 --- a/sqlspec/adapters/mysqlconnector/adk/store.py +++ b/sqlspec/adapters/mysqlconnector/adk/store.py @@ -3,10 +3,9 @@ import re from typing import TYPE_CHECKING, Any, Final, cast -import mysql.connector - from sqlspec.extensions.adk import BaseAsyncADKStore, BaseSyncADKStore, EventRecord, SessionRecord from sqlspec.extensions.adk.memory.store import BaseAsyncADKMemoryStore, BaseSyncADKMemoryStore +from sqlspec.protocols import HasErrnoProtocol from sqlspec.utils.serializers import from_json, to_json if TYPE_CHECKING: @@ -77,6 +76,8 @@ async def create_session( async def get_session( self, app_name: str, user_id: str, session_id: str, *, renew_for: "int | timedelta | None" = None ) -> "SessionRecord | None": + import mysql.connector + try: async with self._config.provide_connection() as conn: cursor = await conn.cursor() @@ -125,6 +126,8 @@ async def update_session_state(self, app_name: str, user_id: str, session_id: st await conn.commit() async def list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]": + import mysql.connector + if user_id is None: sql = f""" SELECT id, app_name, user_id, state, create_time, update_time @@ -250,6 +253,8 @@ async def get_events( after_timestamp: "datetime | None" = None, limit: "int | None" = None, ) -> "list[EventRecord]": + import mysql.connector + if limit == 0: return [] @@ -309,6 +314,8 @@ async def upsert_user_state(self, app_name: str, user_id: str, state: "dict[str, ) async def get_metadata(self, key: str) -> "str | None": + import mysql.connector + sql = f"SELECT value FROM {self._metadata_table} WHERE `key` = %s" try: async with self._config.provide_connection() as conn: @@ -506,6 +513,8 @@ def _create_session( def _get_session( self, app_name: str, user_id: str, session_id: str, *, renew_for: "int | timedelta | None" = None ) -> "SessionRecord | None": + import mysql.connector + try: with self._config.provide_connection() as conn: cursor = conn.cursor() @@ -553,6 +562,8 @@ def _update_session_state(self, app_name: str, user_id: str, session_id: str, st conn.commit() def _list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]": + import mysql.connector + if user_id is None: sql = f""" SELECT id, app_name, user_id, state, create_time, update_time @@ -678,6 +689,8 @@ def _get_events( after_timestamp: "datetime | None" = None, limit: "int | None" = None, ) -> "list[EventRecord]": + import mysql.connector + if limit == 0: return [] @@ -737,6 +750,8 @@ def _upsert_user_state(self, app_name: str, user_id: str, state: "dict[str, Any] ) def _get_metadata(self, key: str) -> "str | None": + import mysql.connector + sql = f"SELECT value FROM {self._metadata_table} WHERE `key` = %s" try: with self._config.provide_connection() as conn: @@ -1205,8 +1220,8 @@ def _parse_owner_id_column_for_mysql(column_ddl: str) -> "tuple[str, str]": def _is_mysql_table_missing(exc: BaseException) -> bool: - args = getattr(exc, "args", ()) - errno = getattr(exc, "errno", None) + args = exc.args + errno = exc.errno if isinstance(exc, HasErrnoProtocol) else None return ( errno == MYSQL_TABLE_NOT_FOUND_ERROR or "doesn't exist" in str(exc) @@ -1264,6 +1279,8 @@ def _raise_session_not_found(session_id: str) -> None: async def _mysqlconnector_async_delete_by_timestamp( store: MysqlConnectorAsyncADKStore, table_name: str, column_name: str, threshold: "datetime" ) -> int: + import mysql.connector + sql = f"DELETE FROM {table_name} WHERE {column_name} < %s" try: async with store._config.provide_connection() as conn: @@ -1285,6 +1302,8 @@ async def _mysqlconnector_async_delete_by_timestamp( async def _mysqlconnector_async_get_state( store: MysqlConnectorAsyncADKStore, table_name: str, where_clause: str, params: "tuple[Any, ...]" ) -> "dict[str, Any] | None": + import mysql.connector + sql = f"SELECT state FROM {table_name} WHERE {where_clause} LIMIT 1" try: async with store._config.provide_connection() as conn: @@ -1316,6 +1335,8 @@ async def _mysqlconnector_async_execute_commit( def _mysqlconnector_sync_delete_by_timestamp( store: MysqlConnectorSyncADKStore, table_name: str, column_name: str, threshold: "datetime" ) -> int: + import mysql.connector + sql = f"DELETE FROM {table_name} WHERE {column_name} < %s" try: with store._config.provide_connection() as conn: @@ -1337,6 +1358,8 @@ def _mysqlconnector_sync_delete_by_timestamp( def _mysqlconnector_sync_get_state( store: MysqlConnectorSyncADKStore, table_name: str, where_clause: str, params: "tuple[Any, ...]" ) -> "dict[str, Any] | None": + import mysql.connector + sql = f"SELECT state FROM {table_name} WHERE {where_clause} LIMIT 1" try: with store._config.provide_connection() as conn: diff --git a/sqlspec/adapters/mysqlconnector/core.py b/sqlspec/adapters/mysqlconnector/core.py index 3c1667935..4de9c909e 100644 --- a/sqlspec/adapters/mysqlconnector/core.py +++ b/sqlspec/adapters/mysqlconnector/core.py @@ -26,6 +26,7 @@ TransactionError, UniqueViolationError, ) +from sqlspec.protocols import HasSqlStateProtocol, HasTypeCodeProtocol from sqlspec.utils.serializers import from_json, to_json from sqlspec.utils.text import quote_backtick_identifier, split_qualified_identifier from sqlspec.utils.type_converters import build_uuid_coercions @@ -356,15 +357,13 @@ def create_mapped_exception(error: Any, *, logger: Any | None = None) -> "SQLSpe True to suppress expected migration errors, or a SQLSpec exception """ error_code = getattr(error, "errno", None) - if error_code is None and hasattr(error, "args") and error.args: + if error_code is None and isinstance(error, Exception) and error.args: value = error.args[0] if isinstance(value, int): error_code = value - sqlstate_attr = getattr(error, "sqlstate", None) - sqlstate = sqlstate_attr if isinstance(sqlstate_attr, str) else None + sqlstate = error.sqlstate if isinstance(error, HasSqlStateProtocol) else None sqlstate_prefix = sqlstate[:2] if isinstance(sqlstate, str) and sqlstate else None - # Migration-specific errors to suppress if error_code in _MYSQL_MIGRATION_ERROR_CODES: if logger is not None: logger.warning("MysqlConnector expected migration error (ignoring): %s", error) @@ -436,7 +435,7 @@ def detect_json_columns_from_description( if isinstance(column, (tuple, list)): type_code = column[1] if len(column) > 1 else None else: - type_code = getattr(column, "type_code", None) + type_code = column.type_code if isinstance(column, HasTypeCodeProtocol) else None if type_code in json_type_codes: append(index) return json_indexes diff --git a/sqlspec/adapters/mysqlconnector/driver.py b/sqlspec/adapters/mysqlconnector/driver.py index d13e14bf5..7dbda497a 100644 --- a/sqlspec/adapters/mysqlconnector/driver.py +++ b/sqlspec/adapters/mysqlconnector/driver.py @@ -12,6 +12,12 @@ import mysql.connector from mysql.connector.constants import FieldType +from sqlspec.adapters.mysqlconnector._typing import ( + MysqlConnectorAsyncCursor, + MysqlConnectorAsyncSessionContext, + MysqlConnectorSyncCursor, + MysqlConnectorSyncSessionContext, +) from sqlspec.adapters.mysqlconnector.core import ( MysqlConnectorAsyncStreamSource, MysqlConnectorSyncStreamSource, @@ -57,13 +63,6 @@ from sqlspec.driver import ExecutionResult from sqlspec.storage import StorageBridgeJob, StorageDestination, StorageFormat, StorageTelemetry -from sqlspec.adapters.mysqlconnector._typing import ( - MysqlConnectorAsyncCursor, - MysqlConnectorAsyncSessionContext, - MysqlConnectorSyncCursor, - MysqlConnectorSyncSessionContext, -) - __all__ = ( "MysqlConnectorAsyncCursor", "MysqlConnectorAsyncDriver", diff --git a/sqlspec/adapters/mysqlconnector/litestar/store.py b/sqlspec/adapters/mysqlconnector/litestar/store.py index f9a0db8ee..58d371d4e 100644 --- a/sqlspec/adapters/mysqlconnector/litestar/store.py +++ b/sqlspec/adapters/mysqlconnector/litestar/store.py @@ -3,8 +3,6 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Final, cast -import mysql.connector - from sqlspec.extensions.litestar.store import BaseSQLSpecStore from sqlspec.utils.logging import get_logger from sqlspec.utils.sync_tools import async_ @@ -53,6 +51,8 @@ async def create_table(self) -> None: self._log_table_created() async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + import mysql.connector + sql = f""" SELECT data, expires_at FROM {self._table_name} WHERE session_id = %s @@ -130,6 +130,8 @@ async def delete(self, key: str) -> None: await conn.commit() async def delete_all(self) -> None: + import mysql.connector + sql = f"DELETE FROM {self._table_name}" try: @@ -148,6 +150,8 @@ async def delete_all(self) -> None: raise async def exists(self, key: str) -> bool: + import mysql.connector + sql = f""" SELECT 1 FROM {self._table_name} WHERE session_id = %s @@ -248,6 +252,8 @@ async def create_table(self) -> None: await async_(self._create_table)() def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + import mysql.connector + sql = f""" SELECT data, expires_at FROM {self._table_name} WHERE session_id = %s @@ -334,6 +340,8 @@ async def delete(self, key: str) -> None: await async_(self._delete)(key) def _delete_all(self) -> None: + import mysql.connector + sql = f"DELETE FROM {self._table_name}" try: @@ -355,6 +363,8 @@ async def delete_all(self) -> None: await async_(self._delete_all)() def _exists(self, key: str) -> bool: + import mysql.connector + sql = f""" SELECT 1 FROM {self._table_name} WHERE session_id = %s diff --git a/sqlspec/adapters/oracledb/_json_handlers.py b/sqlspec/adapters/oracledb/_json_handlers.py index 623f282bd..31e27187a 100644 --- a/sqlspec/adapters/oracledb/_json_handlers.py +++ b/sqlspec/adapters/oracledb/_json_handlers.py @@ -29,7 +29,7 @@ """ from functools import partial -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from sqlspec.adapters.oracledb._typing import DB_TYPE_BLOB, DB_TYPE_CLOB, DB_TYPE_JSON from sqlspec.utils.serializers import from_json, to_json @@ -89,16 +89,17 @@ def _is_json_payload(value: Any) -> bool: ``dict`` and ``tuple``/``list`` of dicts are claimed. Sequences whose first element is a number are NOT claimed — those are vector embeddings and belong to the vector handler. + + An empty sequence is ambiguous (could be empty vector or empty list) and + defers to the next handler in the chain. Sequences of numbers (vector + embeddings) are rejected. """ if isinstance(value, dict): return True if isinstance(value, (list, tuple)): if not value: - # Empty sequence: ambiguous (could be empty vector or empty list). - # Defer to the next handler in the chain. return False first = value[0] - # Reject sequences of numbers (vector embeddings). return not (isinstance(first, (int, float)) and not isinstance(first, bool)) return False @@ -108,7 +109,7 @@ def _input_type_handler(cursor: "Cursor | AsyncCursor", value: Any, arraysize: i if not _is_json_payload(value): return None - server_major = getattr(cursor.connection, "_sqlspec_oracle_major", None) + server_major = cast("int | None", getattr(cursor.connection, "_sqlspec_oracle_major", None)) if server_major is None or server_major >= _NATIVE_JSON_MIN_MAJOR: return cursor.var(DB_TYPE_JSON, arraysize=arraysize) @@ -118,11 +119,14 @@ def _input_type_handler(cursor: "Cursor | AsyncCursor", value: Any, arraysize: i def _output_type_handler(cursor: "Cursor | AsyncCursor", metadata: Any) -> Any: - """Oracle output type handler for JSON-bearing column reads.""" + """Oracle output type handler for JSON-bearing column reads. + + For native JSON columns (DB_TYPE_JSON), python-oracledb returns dict/list + directly, so no conversion is needed. + """ type_code = getattr(metadata, "type_code", None) if type_code is DB_TYPE_JSON: - # Native JSON: python-oracledb returns dict/list directly. No conversion. return None type_name = (getattr(metadata, "type_name", "") or "").upper() diff --git a/sqlspec/adapters/oracledb/_typing.py b/sqlspec/adapters/oracledb/_typing.py index 7f2c133c0..dcd176ef9 100644 --- a/sqlspec/adapters/oracledb/_typing.py +++ b/sqlspec/adapters/oracledb/_typing.py @@ -18,6 +18,7 @@ Connection, Cursor, DatabaseError, + Error, ) from oracledb.pool import AsyncConnectionPool, ConnectionPool @@ -64,6 +65,7 @@ "SPARSE_VECTOR_TYPE", "AQDequeueOptions", "DatabaseError", + "Error", "OracleAsyncConnection", "OracleAsyncConnectionPool", "OracleAsyncCursor", @@ -75,6 +77,7 @@ "OracleSyncCursor", "OracleSyncRawCursor", "OracleSyncSessionContext", + "create_pipeline", ) AQDequeueOptions: Any | None = getattr(_oracledb, "AQDequeueOptions", None) @@ -82,6 +85,7 @@ AQMSG_INVISIBLE: int | None = getattr(_oracledb, "AQMSG_INVISIBLE", None) AQMSG_PAYLOAD_TYPE_JSON: Any | None = getattr(_oracledb, "AQMSG_PAYLOAD_TYPE_JSON", None) SPARSE_VECTOR_TYPE: type[object] | None = getattr(_oracledb, "SparseVector", None) +create_pipeline = _oracledb.create_pipeline class OracleSyncCursor: diff --git a/sqlspec/adapters/oracledb/_vector_handlers.py b/sqlspec/adapters/oracledb/_vector_handlers.py index fcbe8ee28..0c6876c3f 100644 --- a/sqlspec/adapters/oracledb/_vector_handlers.py +++ b/sqlspec/adapters/oracledb/_vector_handlers.py @@ -11,7 +11,7 @@ """ import array -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from sqlspec.adapters.oracledb._typing import DB_TYPE_VECTOR, SPARSE_VECTOR_TYPE from sqlspec.typing import NUMPY_INSTALLED @@ -192,7 +192,7 @@ def _output_type_handler(cursor: "Cursor | AsyncCursor", metadata: Any) -> Any: if getattr(metadata, "vector_is_sparse", False) is True: return None - fmt = getattr(cursor.connection, "_sqlspec_vector_return_format", None) + fmt = cast("str | None", getattr(cursor.connection, "_sqlspec_vector_return_format", None)) if fmt is None: fmt = _VECTOR_RETURN_NUMPY if NUMPY_INSTALLED else _VECTOR_RETURN_LIST diff --git a/sqlspec/adapters/oracledb/driver.py b/sqlspec/adapters/oracledb/driver.py index c29f8ac6c..cb163630c 100644 --- a/sqlspec/adapters/oracledb/driver.py +++ b/sqlspec/adapters/oracledb/driver.py @@ -3,9 +3,9 @@ import logging from typing import TYPE_CHECKING, Any, NamedTuple, cast -import oracledb - from sqlspec.adapters.oracledb._typing import ( + DB_TYPE_BLOB, + DB_TYPE_CLOB, OracleAsyncConnection, OracleAsyncCursor, OracleAsyncSessionContext, @@ -13,6 +13,9 @@ OracleSyncCursor, OracleSyncSessionContext, ) +from sqlspec.adapters.oracledb._typing import DatabaseError as OracleDatabaseError +from sqlspec.adapters.oracledb._typing import Error as OracleError +from sqlspec.adapters.oracledb._typing import create_pipeline as create_oracle_pipeline from sqlspec.adapters.oracledb.core import ( ORACLEDB_VERSION, OracleAsyncStreamSource, @@ -265,7 +268,7 @@ class OracleSyncExceptionHandler(BaseSyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: if exc_type is None: return False - if issubclass(exc_type, oracledb.DatabaseError): + if issubclass(exc_type, OracleDatabaseError): self.pending_exception = create_mapped_exception(exc_val) return True return False @@ -287,7 +290,7 @@ class OracleAsyncExceptionHandler(BaseAsyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: if exc_type is None: return False - if issubclass(exc_type, oracledb.DatabaseError): + if issubclass(exc_type, OracleDatabaseError): self.pending_exception = create_mapped_exception(exc_val) return True return False @@ -334,6 +337,10 @@ def __init__( def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": """Execute single SQL statement with Oracle data handling. + For SELECT-like statements, fetches all rows, resolves row metadata, and + applies LOB coercion if needed. For non-SELECT statements, resolves and + returns the affected row count. + Args: cursor: Oracle cursor object statement: SQL statement to execute @@ -346,8 +353,8 @@ def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": prepared_parameters = coerce_large_parameters_sync( self.connection, prepared_parameters, - clob_type=oracledb.DB_TYPE_CLOB, - blob_type=oracledb.DB_TYPE_BLOB, + clob_type=DB_TYPE_CLOB, + blob_type=DB_TYPE_BLOB, varchar2_byte_limit=self.driver_features.get("oracle_varchar2_byte_limit", 4000), raw_byte_limit=self.driver_features.get("oracle_raw_byte_limit", 2000), ) @@ -355,7 +362,6 @@ def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": cursor.execute(sql, prepared_parameters or {}, **build_fetch_kwargs(self.driver_features)) - # SELECT result processing for Oracle is_select_like = statement.returns_rows() or self._should_force_select(statement, cursor) if is_select_like: @@ -378,7 +384,6 @@ def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": row_format="tuple", ) - # Non-SELECT result processing affected_rows = resolve_rowcount(cursor) return self.create_execution_result(cursor, rowcount_override=affected_rows) @@ -407,9 +412,9 @@ def dispatch_execute_many(self, cursor: Any, statement: "SQL") -> "ExecutionResu if batch_errors: special_data["oracle_batch_errors"] = [ { - "offset": getattr(error, "offset", None), - "code": getattr(error, "code", None), - "message": getattr(error, "message", str(error)), + "offset": cast("int | None", getattr(error, "offset", None)), + "code": cast("int | None", getattr(error, "code", None)), + "message": cast("str", getattr(error, "message", str(error))), } for error in cursor.getbatcherrors() ] @@ -468,7 +473,7 @@ def commit(self) -> None: """ try: self.connection.commit() - except oracledb.Error as e: + except OracleError as e: msg = f"Failed to commit Oracle transaction: {e}" raise SQLSpecError(msg) from e @@ -480,7 +485,7 @@ def rollback(self) -> None: """ try: self.connection.rollback() - except oracledb.Error as e: + except OracleError as e: msg = f"Failed to rollback Oracle transaction: {e}" raise SQLSpecError(msg) from e @@ -818,7 +823,7 @@ def _fetch_arrow_record_batches( def _execute_stack_native(self, stack: "StatementStack", *, continue_on_error: bool) -> "tuple[StackResult, ...]": compiled_operations = [self._prepare_pipeline_operation(op) for op in stack.operations] - pipeline = oracledb.create_pipeline() + pipeline = create_oracle_pipeline() for compiled in compiled_operations: self._add_pipeline_operation(pipeline, compiled) @@ -919,6 +924,10 @@ def __init__( async def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult": """Execute single SQL statement with Oracle data handling. + For SELECT-like statements, fetches all rows, resolves row metadata, and + applies LOB coercion if needed. For non-SELECT statements, resolves and + returns the affected row count. + Args: cursor: Oracle cursor object statement: SQL statement to execute @@ -931,8 +940,8 @@ async def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionRes prepared_parameters = await coerce_large_parameters_async( self.connection, prepared_parameters, - clob_type=oracledb.DB_TYPE_CLOB, - blob_type=oracledb.DB_TYPE_BLOB, + clob_type=DB_TYPE_CLOB, + blob_type=DB_TYPE_BLOB, varchar2_byte_limit=self.driver_features.get("oracle_varchar2_byte_limit", 4000), raw_byte_limit=self.driver_features.get("oracle_raw_byte_limit", 2000), ) @@ -940,7 +949,6 @@ async def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionRes await cursor.execute(sql, prepared_parameters or {}, **build_fetch_kwargs(self.driver_features)) - # SELECT result processing for Oracle is_select_like = statement.returns_rows() or self._should_force_select(statement, cursor) if is_select_like: @@ -963,7 +971,6 @@ async def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionRes row_format="tuple", ) - # Non-SELECT result processing affected_rows = resolve_rowcount(cursor) return self.create_execution_result(cursor, rowcount_override=affected_rows) @@ -994,9 +1001,9 @@ async def dispatch_execute_many(self, cursor: Any, statement: "SQL") -> "Executi if batch_errors: special_data["oracle_batch_errors"] = [ { - "offset": getattr(error, "offset", None), - "code": getattr(error, "code", None), - "message": getattr(error, "message", str(error)), + "offset": cast("int | None", getattr(error, "offset", None)), + "code": cast("int | None", getattr(error, "code", None)), + "message": cast("str", getattr(error, "message", str(error))), } for error in cursor.getbatcherrors() ] @@ -1055,7 +1062,7 @@ async def commit(self) -> None: """ try: await self.connection.commit() - except oracledb.Error as e: + except OracleError as e: msg = f"Failed to commit Oracle transaction: {e}" raise SQLSpecError(msg) from e @@ -1067,7 +1074,7 @@ async def rollback(self) -> None: """ try: await self.connection.rollback() - except oracledb.Error as e: + except OracleError as e: msg = f"Failed to rollback Oracle transaction: {e}" raise SQLSpecError(msg) from e @@ -1418,7 +1425,7 @@ async def _execute_stack_native( self, stack: "StatementStack", *, continue_on_error: bool ) -> "tuple[StackResult, ...]": compiled_operations = [self._prepare_pipeline_operation(op) for op in stack.operations] - pipeline = oracledb.create_pipeline() + pipeline = create_oracle_pipeline() for compiled in compiled_operations: self._add_pipeline_operation(pipeline, compiled) diff --git a/sqlspec/adapters/psqlpy/_typing.py b/sqlspec/adapters/psqlpy/_typing.py index 70424a7eb..6da2549d3 100644 --- a/sqlspec/adapters/psqlpy/_typing.py +++ b/sqlspec/adapters/psqlpy/_typing.py @@ -6,25 +6,45 @@ from typing import TYPE_CHECKING, Any -from psqlpy import Connection as _PsqlpyConnection -from psqlpy import Listener as _PsqlpyListener +from sqlspec.typing import import_optional_attr + + +class _PsqlpyUnavailableError(Exception): + """Fallback psqlpy exception base when optional exception classes are unavailable.""" + if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType from typing import TypeAlias + from psqlpy import Connection as _PsqlpyConnection + from psqlpy import Listener as _PsqlpyListener + from psqlpy.exceptions import DatabaseError as _PsqlpyDatabaseError + from psqlpy.exceptions import Error as _PsqlpyError + from sqlspec.adapters.psqlpy.driver import PsqlpyDriver from sqlspec.core import StatementConfig PsqlpyConnection: TypeAlias = _PsqlpyConnection + PsqlpyDatabaseError: TypeAlias = _PsqlpyDatabaseError + PsqlpyError: TypeAlias = _PsqlpyError PsqlpyListener: TypeAlias = _PsqlpyListener if not TYPE_CHECKING: - PsqlpyConnection = _PsqlpyConnection - PsqlpyListener = _PsqlpyListener - -__all__ = ("PsqlpyConnection", "PsqlpyCursor", "PsqlpyListener", "PsqlpySessionContext") + PsqlpyConnection = import_optional_attr("psqlpy", "Connection") or Any + PsqlpyDatabaseError = import_optional_attr("psqlpy.exceptions", "DatabaseError") or _PsqlpyUnavailableError + PsqlpyError = import_optional_attr("psqlpy.exceptions", "Error") or _PsqlpyUnavailableError + PsqlpyListener = import_optional_attr("psqlpy", "Listener") or Any + +__all__ = ( + "PsqlpyConnection", + "PsqlpyCursor", + "PsqlpyDatabaseError", + "PsqlpyError", + "PsqlpyListener", + "PsqlpySessionContext", +) class PsqlpyCursor: diff --git a/sqlspec/adapters/psqlpy/adk/store.py b/sqlspec/adapters/psqlpy/adk/store.py index 6c7f5768a..75522b614 100644 --- a/sqlspec/adapters/psqlpy/adk/store.py +++ b/sqlspec/adapters/psqlpy/adk/store.py @@ -3,8 +3,6 @@ import re from typing import TYPE_CHECKING, Any, Final, NoReturn -import psqlpy.exceptions - from sqlspec.extensions.adk import BaseAsyncADKStore, EventRecord, SessionRecord from sqlspec.extensions.adk.memory.store import BaseAsyncADKMemoryStore from sqlspec.utils.logging import get_logger @@ -22,10 +20,6 @@ logger = get_logger("sqlspec.adapters.psqlpy.adk.store") POSTGRES_TABLE_NOT_FOUND_SQLSTATE: Final = "42P01" -PSQLPY_TABLE_MISSING_ERRORS: Final[tuple[type[Exception], ...]] = ( - psqlpy.exceptions.DatabaseError, - psqlpy.exceptions.ConnectionExecuteError, -) PSQLPY_STATUS_REGEX: Final[re.Pattern[str]] = re.compile(r"^([A-Z]+)(?:\s+(\d+))?\s+(\d+)$", re.IGNORECASE) @@ -122,7 +116,7 @@ async def get_session( create_time=row["create_time"], update_time=row["update_time"], ) - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return None raise @@ -171,7 +165,7 @@ async def list_sessions(self, app_name: str, user_id: str | None = None) -> "lis ) for row in rows ] - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return [] raise @@ -322,7 +316,7 @@ async def get_events( ) for row in rows ] - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return [] raise @@ -338,7 +332,7 @@ async def delete_expired_events(self, before: "datetime") -> int: count = int(count_rows[0]["count"]) if count_rows else 0 await conn.execute(delete_sql, [before]) return count - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return 0 raise @@ -354,7 +348,7 @@ async def delete_idle_sessions(self, updated_before: "datetime") -> int: count = int(count_rows[0]["count"]) if count_rows else 0 await conn.execute(delete_sql, [updated_before]) return count - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return 0 raise @@ -367,7 +361,7 @@ async def get_app_state(self, app_name: str) -> "dict[str, Any] | None": result = await conn.fetch(sql, [app_name]) rows: list[dict[str, Any]] = result.result() if result else [] return rows[0]["state"] if rows else None - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return None raise @@ -380,7 +374,7 @@ async def get_user_state(self, app_name: str, user_id: str) -> "dict[str, Any] | result = await conn.fetch(sql, [app_name, user_id]) rows: list[dict[str, Any]] = result.result() if result else [] return rows[0]["state"] if rows else None - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return None raise @@ -417,7 +411,7 @@ async def get_metadata(self, key: str) -> "str | None": result = await conn.fetch(sql, [key]) rows: list[dict[str, Any]] = result.result() if result else [] return rows[0]["value"] if rows else None - except PSQLPY_TABLE_MISSING_ERRORS as e: + except Exception as e: if _is_table_missing_error(e): return None raise @@ -632,10 +626,11 @@ async def search_entries( except Exception as exc: # pragma: no cover logger.warning("FTS search failed; falling back to simple search: %s", exc) return await self._search_entries_simple(query, app_name, user_id, effective_limit) - except psqlpy.exceptions.DatabaseError as e: - error_msg = str(e).lower() - if "does not exist" in error_msg or "relation" in error_msg: - return [] + except Exception as e: + if _is_psqlpy_database_error(e): + error_msg = str(e).lower() + if "does not exist" in error_msg or "relation" in error_msg: + return [] raise async def delete_entries_by_session(self, session_id: str) -> int: @@ -650,10 +645,11 @@ async def delete_entries_by_session(self, session_id: str) -> int: count = int(count_rows[0]["count"]) if count_rows else 0 await conn.execute(delete_sql, [session_id]) return count - except psqlpy.exceptions.DatabaseError as e: - error_msg = str(e).lower() - if "does not exist" in error_msg or "relation" in error_msg: - return 0 + except Exception as e: + if _is_psqlpy_database_error(e): + error_msg = str(e).lower() + if "does not exist" in error_msg or "relation" in error_msg: + return 0 raise async def delete_entries_older_than(self, days: int) -> int: @@ -674,10 +670,11 @@ async def delete_entries_older_than(self, days: int) -> int: count = int(count_rows[0]["count"]) if count_rows else 0 await conn.execute(delete_sql, []) return count - except psqlpy.exceptions.DatabaseError as e: - error_msg = str(e).lower() - if "does not exist" in error_msg or "relation" in error_msg: - return 0 + except Exception as e: + if _is_psqlpy_database_error(e): + error_msg = str(e).lower() + if "does not exist" in error_msg or "relation" in error_msg: + return 0 raise async def _get_create_memory_table_sql(self) -> str: @@ -804,7 +801,21 @@ def _rows_to_records(rows: "list[dict[str, Any]]") -> "list[MemoryRecord]": ] +def _is_psqlpy_database_error(exc: Exception) -> bool: + try: + import psqlpy.exceptions + except ImportError: + return False + return isinstance(exc, psqlpy.exceptions.DatabaseError) + + def _is_table_missing_error(exc: Exception) -> bool: + try: + import psqlpy.exceptions + except ImportError: + return False + if not isinstance(exc, (psqlpy.exceptions.DatabaseError, psqlpy.exceptions.ConnectionExecuteError)): + return False error_msg = str(exc).lower() return "does not exist" in error_msg or "relation" in error_msg diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py index cae438ea0..d3e853f52 100644 --- a/sqlspec/adapters/psqlpy/config.py +++ b/sqlspec/adapters/psqlpy/config.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast from mypy_extensions import mypyc_attr -from psqlpy import ConnectionPool from typing_extensions import NotRequired from sqlspec.adapters.psqlpy._typing import PsqlpyConnection, PsqlpyCursor, PsqlpySessionContext @@ -25,6 +24,8 @@ from collections.abc import Awaitable, Callable from types import TracebackType + from psqlpy import ConnectionPool + from sqlspec.core import StatementConfig __all__ = ("PsqlpyConfig", "PsqlpyConnectionParams", "PsqlpyCursor", "PsqlpyDriverFeatures", "PsqlpyPoolParams") @@ -179,7 +180,7 @@ async def __aexit__( @mypyc_attr(native_class=False) -class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyDriver]): +class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, "ConnectionPool", PsqlpyDriver]): """Configuration for Psqlpy asynchronous database connections.""" driver_type: ClassVar[type[PsqlpyDriver]] = PsqlpyDriver @@ -200,7 +201,7 @@ def __init__( self, *, connection_config: "PsqlpyPoolParams | dict[str, Any] | None" = None, - connection_instance: ConnectionPool | None = None, + connection_instance: "ConnectionPool | None" = None, migration_config: "dict[str, Any] | None" = None, statement_config: "StatementConfig | None" = None, driver_features: "PsqlpyDriverFeatures | dict[str, Any] | None" = None, @@ -210,6 +211,10 @@ def __init__( ) -> None: """Initialize Psqlpy configuration. + Extracts the 'on_connection_create' hook from driver_features before + storing them. Initializes a set to track initialized connection IDs + because psqlpy connections do not support weak references. + Args: connection_config: Connection and pool configuration parameters. connection_instance: Existing connection pool instance to use. @@ -225,12 +230,10 @@ def __init__( statement_config = statement_config or default_statement_config statement_config, driver_features = apply_driver_features(statement_config, driver_features) - # Extract user connection hook before storing driver_features features_dict = dict(driver_features) if driver_features else {} self._user_connection_hook: Callable[[PsqlpyConnection], Awaitable[None]] | None = features_dict.pop( "on_connection_create", None ) - # Track initialized connections by ID (psqlpy connections don't support weak refs) self._initialized_connection_ids: set[int] = set() self._pgvector_available: bool | None = None self._paradedb_available: bool | None = None @@ -247,8 +250,10 @@ def __init__( ) async def _ensure_connection_initialized(self, connection: "PsqlpyConnection") -> None: - """Ensure connection callback has been called exactly once for this connection.""" - # Detect extensions on first connection, update dialect + """Ensure connection callback has been called exactly once for this connection. + + Detects PostgreSQL extensions on first connection and updates dialect accordingly. + """ if self._pgvector_available is None: detected_extensions: set[str] = set() extensions = build_postgres_extension_probe_names(self.driver_features) @@ -274,6 +279,8 @@ async def _ensure_connection_initialized(self, connection: "PsqlpyConnection") - async def _create_pool(self) -> "ConnectionPool": """Create the actual async connection pool.""" + from psqlpy import ConnectionPool + return ConnectionPool(**build_connection_config(self.connection_config)) async def _close_pool(self) -> None: @@ -320,7 +327,7 @@ def provide_session( prepare_driver=self._prepare_driver, ) - async def provide_pool(self, *args: Any, **kwargs: Any) -> ConnectionPool: + async def provide_pool(self, *args: Any, **kwargs: Any) -> "ConnectionPool": """Provide async pool instance. Returns: diff --git a/sqlspec/adapters/psqlpy/core.py b/sqlspec/adapters/psqlpy/core.py index ba5a4510e..9581b6ad7 100644 --- a/sqlspec/adapters/psqlpy/core.py +++ b/sqlspec/adapters/psqlpy/core.py @@ -561,6 +561,14 @@ def create_mapped_exception(error: Any) -> SQLSpecError: psqlpy doesn't expose SQLSTATE codes directly, so we rely on message-based pattern matching for exception classification. + Mapped Exceptions: + * Integrity constraint violations (UniqueViolationError, ForeignKeyViolationError, etc.) + * Transaction/serialization errors (DeadlockError, SerializationConflictError) + * QueryTimeoutError: cancellations, timeouts + * PermissionDeniedError: permission denied, authentication failed + * ConnectionTimeoutError / DatabaseConnectionError: connection errors + * SQLParsingError: syntax/parse errors + Args: error: The psqlpy exception to map @@ -569,7 +577,6 @@ def create_mapped_exception(error: Any) -> SQLSpecError: """ error_msg = str(error).lower() - # Integrity constraint violations (most specific first) if "unique" in error_msg or "duplicate key" in error_msg: return _create_postgres_error(error, UniqueViolationError, "unique constraint violation") if "foreign key" in error_msg or "violates foreign key" in error_msg: @@ -581,29 +588,24 @@ def create_mapped_exception(error: Any) -> SQLSpecError: if "constraint" in error_msg: return _create_postgres_error(error, IntegrityError, "integrity constraint violation") - # Transaction and serialization errors (deadlock before serialization) if "deadlock" in error_msg: return _create_postgres_error(error, DeadlockError, "deadlock detected") if "serialization failure" in error_msg or "could not serialize" in error_msg: return _create_postgres_error(error, SerializationConflictError, "serialization failure") - # Query timeout/cancellation if "cancel" in error_msg or "timeout" in error_msg or "statement timeout" in error_msg: return _create_postgres_error(error, QueryTimeoutError, "query canceled or timed out") - # Permission/authentication errors if "permission denied" in error_msg or "insufficient privilege" in error_msg: return _create_postgres_error(error, PermissionDeniedError, "permission denied") if "authentication failed" in error_msg or "password" in error_msg: return _create_postgres_error(error, PermissionDeniedError, "authentication error") - # Connection errors if "connection" in error_msg or "could not connect" in error_msg: if "timeout" in error_msg: return _create_postgres_error(error, ConnectionTimeoutError, "connection timeout") return _create_postgres_error(error, DatabaseConnectionError, "connection error") - # SQL syntax errors if "syntax error" in error_msg or "parse" in error_msg: return _create_postgres_error(error, SQLParsingError, "SQL syntax error") diff --git a/sqlspec/adapters/psqlpy/data_dictionary.py b/sqlspec/adapters/psqlpy/data_dictionary.py index 2724b31d7..6a4900d87 100644 --- a/sqlspec/adapters/psqlpy/data_dictionary.py +++ b/sqlspec/adapters/psqlpy/data_dictionary.py @@ -24,12 +24,14 @@ def __init__(self) -> None: super().__init__() async def get_version(self, driver: "PsqlpyDriver") -> "VersionInfo | None": - """Get PostgreSQL database version information.""" + """Get PostgreSQL database version information. + + Performs an inline cache check first to avoid a cross-module method call + that causes mypyc segfaults. If not cached, fetches from the database. + """ driver_id = id(driver) - # Inline cache check to avoid cross-module method call that causes mypyc segfault if driver_id in self._version_fetch_attempted: return self._version_cache.get(driver_id) - # Not cached, fetch from database version_value = await driver.select_value(self.get_query("version")) if not version_value: diff --git a/sqlspec/adapters/psqlpy/driver.py b/sqlspec/adapters/psqlpy/driver.py index 04108c4fc..e16f92333 100644 --- a/sqlspec/adapters/psqlpy/driver.py +++ b/sqlspec/adapters/psqlpy/driver.py @@ -7,9 +7,7 @@ import inspect from typing import TYPE_CHECKING, Any, cast -import psqlpy.exceptions - -from sqlspec.adapters.psqlpy._typing import PsqlpyCursor, PsqlpySessionContext +from sqlspec.adapters.psqlpy._typing import PsqlpyCursor, PsqlpyDatabaseError, PsqlpyError, PsqlpySessionContext from sqlspec.adapters.psqlpy.core import ( PsqlpyStreamSource, build_insert_statement, @@ -66,7 +64,8 @@ class PsqlpyExceptionHandler(BaseAsyncExceptionHandler): def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "BaseException") -> bool: if exc_type is None: return False - if issubclass(exc_type, (psqlpy.exceptions.DatabaseError, psqlpy.exceptions.Error)): + + if issubclass(exc_type, (PsqlpyDatabaseError, PsqlpyError)): self.pending_exception = create_mapped_exception(exc_val) return True return False @@ -201,7 +200,7 @@ async def begin(self) -> None: """Begin a database transaction.""" try: await self.connection.execute("BEGIN") - except psqlpy.exceptions.DatabaseError as e: + except PsqlpyDatabaseError as e: msg = f"Failed to begin psqlpy transaction: {e}" raise SQLSpecError(msg) from e @@ -209,7 +208,7 @@ async def commit(self) -> None: """Commit the current transaction.""" try: await self.connection.execute("COMMIT") - except psqlpy.exceptions.DatabaseError as e: + except PsqlpyDatabaseError as e: msg = f"Failed to commit psqlpy transaction: {e}" raise SQLSpecError(msg) from e @@ -217,7 +216,7 @@ async def rollback(self) -> None: """Rollback the current transaction.""" try: await self.connection.execute("ROLLBACK") - except psqlpy.exceptions.DatabaseError as e: + except PsqlpyDatabaseError as e: msg = f"Failed to rollback psqlpy transaction: {e}" raise SQLSpecError(msg) from e @@ -334,7 +333,7 @@ async def load_from_arrow( copy_operation = cursor.binary_copy_to_table(copy_payload, table_name, **copy_kwargs) if inspect.isawaitable(copy_operation): await copy_operation - except (TypeError, psqlpy.exceptions.DatabaseError) as exc: + except (TypeError, PsqlpyDatabaseError) as exc: logger.debug("Binary COPY not available for psqlpy; falling back to INSERT statements: %s", exc) insert_sql = build_insert_statement(table, columns) formatted_records = coerce_records_for_execute_many(records) @@ -342,7 +341,7 @@ async def load_from_arrow( insert_operation = cursor.execute_many(insert_sql, formatted_records) if inspect.isawaitable(insert_operation): await insert_operation - except (psqlpy.exceptions.DatabaseError, psqlpy.exceptions.Error) as fallback_exc: + except (PsqlpyDatabaseError, PsqlpyError) as fallback_exc: if "PyJSON must be dict, list, or tuple" not in str(fallback_exc): raise formatted_records = coerce_records_for_execute_many(records, parse_json_text=True) diff --git a/sqlspec/adapters/pymysql/core.py b/sqlspec/adapters/pymysql/core.py index 41ed62d62..4edb948c8 100644 --- a/sqlspec/adapters/pymysql/core.py +++ b/sqlspec/adapters/pymysql/core.py @@ -8,8 +8,6 @@ from collections.abc import Callable, Sized from typing import TYPE_CHECKING, Any -from pymysql.cursors import SSCursor - from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile from sqlspec.driver import rows_to_dicts from sqlspec.exceptions import ( @@ -28,6 +26,7 @@ TransactionError, UniqueViolationError, ) +from sqlspec.protocols import HasSqlStateProtocol, HasTypeCodeProtocol from sqlspec.utils.serializers import from_json, to_json from sqlspec.utils.text import quote_backtick_identifier, split_qualified_identifier from sqlspec.utils.type_converters import build_uuid_coercions @@ -186,6 +185,8 @@ def __init__(self, driver: Any, sql: str, parameters: Any, chunk_size: int) -> N self._column_names: list[str] | None = None def start(self) -> None: + from pymysql.cursors import SSCursor + handler = self._driver.handle_database_exceptions() with handler: cursor = self._driver.connection.cursor(SSCursor) @@ -311,13 +312,11 @@ def create_mapped_exception(error: Any, *, logger: Any | None = None) -> "SQLSpe Returns: True to suppress expected migration errors, or a SQLSpec exception """ - error_args = getattr(error, "args", ()) + error_args = error.args error_code = error_args[0] if error_args and isinstance(error_args[0], int) else None - sqlstate_attr = getattr(error, "sqlstate", None) - sqlstate = sqlstate_attr if isinstance(sqlstate_attr, str) else None + sqlstate = error.sqlstate if isinstance(error, HasSqlStateProtocol) else None sqlstate_prefix = sqlstate[:2] if isinstance(sqlstate, str) and sqlstate else None - # Migration-specific errors to suppress if error_code in _MYSQL_MIGRATION_ERROR_CODES: if logger is not None: logger.warning("PyMySQL expected migration error (ignoring): %s", error) @@ -389,7 +388,7 @@ def detect_json_columns_from_description( if isinstance(column, (tuple, list)): type_code = column[1] if len(column) > 1 else None else: - type_code = getattr(column, "type_code", None) + type_code = column.type_code if isinstance(column, HasTypeCodeProtocol) else None if type_code in json_type_codes: append(index) return json_indexes diff --git a/sqlspec/adapters/pymysql/litestar/store.py b/sqlspec/adapters/pymysql/litestar/store.py index 11c2e773a..d758db917 100644 --- a/sqlspec/adapters/pymysql/litestar/store.py +++ b/sqlspec/adapters/pymysql/litestar/store.py @@ -3,8 +3,6 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Final -import pymysql - from sqlspec.extensions.litestar.store import BaseSQLSpecStore from sqlspec.utils.logging import get_logger from sqlspec.utils.sync_tools import async_ @@ -57,6 +55,8 @@ async def create_table(self) -> None: await async_(self._create_table)() def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None": + import pymysql + sql = f""" SELECT data, expires_at FROM {self._table_name} WHERE session_id = %s @@ -93,7 +93,7 @@ def _get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | return bytes(row["data"]) except pymysql.MySQLError as exc: - if "doesn't exist" in str(exc) or getattr(exc, "args", [None])[0] == MYSQL_TABLE_NOT_FOUND_ERROR: + if "doesn't exist" in str(exc) or (exc.args[0] if exc.args else None) == MYSQL_TABLE_NOT_FOUND_ERROR: return None raise @@ -140,6 +140,8 @@ async def delete(self, key: str) -> None: await async_(self._delete)(key) def _delete_all(self) -> None: + import pymysql + sql = f"DELETE FROM {self._table_name}" try: @@ -152,7 +154,7 @@ def _delete_all(self) -> None: conn.commit() self._log_delete_all() except pymysql.MySQLError as exc: - if "doesn't exist" in str(exc) or getattr(exc, "args", [None])[0] == MYSQL_TABLE_NOT_FOUND_ERROR: + if "doesn't exist" in str(exc) or (exc.args[0] if exc.args else None) == MYSQL_TABLE_NOT_FOUND_ERROR: logger.debug("Table %s does not exist, skipping delete_all", self._table_name) return raise @@ -161,6 +163,8 @@ async def delete_all(self) -> None: await async_(self._delete_all)() def _exists(self, key: str) -> bool: + import pymysql + sql = f""" SELECT 1 FROM {self._table_name} WHERE session_id = %s @@ -177,7 +181,7 @@ def _exists(self, key: str) -> bool: cursor.close() return result is not None except pymysql.MySQLError as exc: - if "doesn't exist" in str(exc) or getattr(exc, "args", [None])[0] == MYSQL_TABLE_NOT_FOUND_ERROR: + if "doesn't exist" in str(exc) or (exc.args[0] if exc.args else None) == MYSQL_TABLE_NOT_FOUND_ERROR: return False raise diff --git a/sqlspec/adapters/spanner/_typing.py b/sqlspec/adapters/spanner/_typing.py index 676ec4a83..c68cd9e3b 100644 --- a/sqlspec/adapters/spanner/_typing.py +++ b/sqlspec/adapters/spanner/_typing.py @@ -6,23 +6,51 @@ from typing import TYPE_CHECKING, Any +from sqlspec.typing import import_optional_attr + + +class _UnavailableSpannerGoogleAPICallError(Exception): + """Fallback Spanner API exception when google-api-core is unavailable.""" + + +class _UnavailableSpannerTransaction: + """Fallback Spanner transaction class when google-cloud-spanner is unavailable.""" + + if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType + from typing import TypeAlias + from google.api_core.exceptions import GoogleAPICallError as _SpannerGoogleAPICallError from google.cloud.spanner_v1.database import SnapshotCheckout from google.cloud.spanner_v1.snapshot import Snapshot - from google.cloud.spanner_v1.transaction import Transaction + from google.cloud.spanner_v1.transaction import Transaction as _SpannerTransaction from sqlspec.adapters.spanner.driver import SpannerSyncDriver from sqlspec.core import StatementConfig - SpannerConnection = Snapshot | SnapshotCheckout | Transaction + SpannerConnection = Snapshot | SnapshotCheckout | _SpannerTransaction + SpannerGoogleAPICallError: TypeAlias = _SpannerGoogleAPICallError + SpannerTransaction: TypeAlias = _SpannerTransaction if not TYPE_CHECKING: SpannerConnection = Any + SpannerGoogleAPICallError = ( + import_optional_attr("google.api_core.exceptions", "GoogleAPICallError") + or _UnavailableSpannerGoogleAPICallError + ) + SpannerTransaction = ( + import_optional_attr("google.cloud.spanner_v1.transaction", "Transaction") or _UnavailableSpannerTransaction + ) -__all__ = ("SpannerConnection", "SpannerSessionContext", "SpannerSyncCursor") +__all__ = ( + "SpannerConnection", + "SpannerGoogleAPICallError", + "SpannerSessionContext", + "SpannerSyncCursor", + "SpannerTransaction", +) class SpannerSyncCursor: diff --git a/sqlspec/adapters/spanner/config.py b/sqlspec/adapters/spanner/config.py index b621d4665..e39604e26 100644 --- a/sqlspec/adapters/spanner/config.py +++ b/sqlspec/adapters/spanner/config.py @@ -2,8 +2,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast -from google.cloud.spanner_v1 import Client -from google.cloud.spanner_v1.pool import AbstractSessionPool, BurstyPool, FixedSizePool, PingingPool from typing_extensions import NotRequired from sqlspec.adapters.spanner._typing import SpannerConnection @@ -26,8 +24,9 @@ from google.api_core.retry import Retry from google.auth.credentials import Credentials from google.cloud.spanner_admin_database_v1.types import DatabaseDialect, EncryptionConfig - from google.cloud.spanner_v1 import DirectedReadOptions, ExecuteSqlRequest, RequestOptions + from google.cloud.spanner_v1 import Client, DirectedReadOptions, ExecuteSqlRequest, RequestOptions from google.cloud.spanner_v1.database import Database + from google.cloud.spanner_v1.pool import AbstractSessionPool from google.cloud.spanner_v1.transaction import DefaultTransactionOptions from sqlspec.config import ExtensionConfigs @@ -207,7 +206,7 @@ def __exit__( txn_id = txn._transaction_id except AttributeError: txn_id = None - mutations = getattr(txn, "_mutations", None) + mutations = cast("list[Any] | None", getattr(txn, "_mutations", None)) try: committed = txn.committed except AttributeError: @@ -289,6 +288,8 @@ def __init__( ): self.connection_config["session_labels"] = legacy_session_labels + from google.cloud.spanner_v1.pool import FixedSizePool + self.connection_config.setdefault("size", self.connection_config.pop("max_sessions", 10)) self.connection_config.setdefault("pool_type", FixedSizePool) @@ -311,7 +312,9 @@ def __init__( self._client: Client | None = None self._database: Database | None = None - def _get_client(self) -> Client: + def _get_client(self) -> "Client": + from google.cloud.spanner_v1 import Client + if self._client is None: client_kwargs = self._resolve_kwargs(_CLIENT_CONFIG_FIELDS) self._client = Client(**client_kwargs) @@ -343,7 +346,9 @@ def get_database(self) -> "Database": def create_connection(self) -> SpannerConnection: return cast("SpannerConnection", self.get_database().snapshot()) # type: ignore[no-untyped-call] - def _create_pool(self) -> AbstractSessionPool: + def _create_pool(self) -> "AbstractSessionPool": + from google.cloud.spanner_v1.pool import BurstyPool, FixedSizePool, PingingPool + instance_id = self.connection_config.get("instance_id") database_id = self.connection_config.get("database_id") if not instance_id or not database_id: @@ -545,3 +550,12 @@ def get_event_runtime_hints(self) -> "EventRuntimeHints": """Return queue defaults for Spanner JSON handling.""" return EventRuntimeHints(json_passthrough=True) + + +def __getattr__(name: str) -> Any: + if name == "Client": + from google.cloud.spanner_v1 import Client + + return Client + msg = f"module {__name__} has no attribute {name}" + raise AttributeError(msg) diff --git a/sqlspec/adapters/spanner/driver.py b/sqlspec/adapters/spanner/driver.py index 38121a8dc..48640ec4f 100644 --- a/sqlspec/adapters/spanner/driver.py +++ b/sqlspec/adapters/spanner/driver.py @@ -4,11 +4,14 @@ from typing import TYPE_CHECKING, Any, Protocol, cast import sqlglot as _sqlglot -from google.api_core import exceptions as api_exceptions -from google.cloud.spanner_v1.transaction import Transaction from sqlglot import exp as _sqlglot_exp -from sqlspec.adapters.spanner._typing import SpannerSessionContext, SpannerSyncCursor +from sqlspec.adapters.spanner._typing import ( + SpannerGoogleAPICallError, + SpannerSessionContext, + SpannerSyncCursor, + SpannerTransaction, +) from sqlspec.adapters.spanner.core import ( build_param_type_signature, coerce_params, @@ -34,6 +37,7 @@ "SpannerSyncConfig.provide_session() opens a write-capable Transaction by default; " "the current session must have been opened via SpannerSyncConfig.provide_read_session()." ) +Transaction = SpannerTransaction if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -110,7 +114,7 @@ def _handle_exception(self, exc_type: "type[BaseException] | None", exc_val: "Ba if exc_type is None: return False - if isinstance(exc_val, api_exceptions.GoogleAPICallError): + if isinstance(exc_val, SpannerGoogleAPICallError): self.pending_exception = create_mapped_exception(exc_val) return True return False @@ -490,8 +494,8 @@ def _chunk_mutation_rows(self, columns: "list[str]", records: "list[tuple[Any, . def _batch_write_mutations(self, table: str, columns: "list[str]", chunks: "list[list[list[Any]]]") -> None: """High-throughput ingest via the Spanner Batch Write API (one mutation group per chunk).""" - session = getattr(self.connection, "_session", None) - database = getattr(session, "_database", None) + session = cast("object", getattr(self.connection, "_session", None)) + database = cast("Any", getattr(session, "_database", None)) if session is not None else None if database is None: msg = "Spanner Batch Write API requires a database-backed session." raise SQLConversionError(msg) @@ -538,21 +542,25 @@ def collect_rows(self, cursor: "SpannerConnection", fetched: "list[Any]") -> "tu Note: Spanner's collect_rows requires result set fields and a type converter. The direct execution path may not always have this metadata available, so this falls back to basic collection. + + For the direct path, if result set fields metadata is not available, + it returns raw data with no column names. If rows are dicts, it attempts + to extract column names from dict keys. For tuple rows without metadata, + it returns them as-is. """ - # For direct path, we need fields metadata from the result set. - # If not available, return raw data with no column names. if not fetched: return [], [], 0 - # Attempt to extract column names from dict keys if rows are dicts if isinstance(fetched[0], dict): column_names = list(fetched[0].keys()) return fetched, column_names, len(fetched) - # For tuple rows without metadata, return as-is return fetched, [], len(fetched) def resolve_rowcount(self, cursor: "SpannerConnection") -> int: - """Resolve rowcount from Spanner cursor for the direct execution path.""" - # Spanner uses execute_update return value, not cursor.rowcount + """Resolve rowcount from Spanner cursor for the direct execution path. + + Spanner uses execute_update return value, not cursor.rowcount, so this + returns 0. + """ return 0 def _connection_in_transaction(self) -> bool: @@ -569,4 +577,11 @@ def _resolve_column_names(self, fields: Any) -> list[str]: return resolve_column_names(fields, self._column_name_cache) +def __getattr__(name: str) -> Any: + if name == "Transaction": + return Transaction + msg = f"module {__name__} has no attribute {name}" + raise AttributeError(msg) + + register_driver_profile("spanner", driver_profile) diff --git a/sqlspec/builder/_case.py b/sqlspec/builder/_case.py new file mode 100644 index 000000000..7d0767c9b --- /dev/null +++ b/sqlspec/builder/_case.py @@ -0,0 +1,53 @@ +"""CASE expression builder and representation. + +Provides structures and builder classes for SQL CASE WHEN expressions. +""" + +from typing import Any, cast + +from sqlglot import exp + +from sqlspec.builder._parsing_utils import parse_condition_expression, to_expression + +__all__ = ("Case", "CaseBuilder") + + +class Case: + """Represent a SQL CASE expression with structured components.""" + + __slots__ = ("conditions", "default") + + def __init__(self, *ifs: exp.Expr, default: exp.Expr | None = None) -> None: + self.conditions = list(ifs) + self.default = default + + def when(self, condition: str | exp.Expr, result: Any) -> "Case": + condition_expr = parse_condition_expression(condition) + result_expr = to_expression(result) + self.conditions.append(exp.If(this=condition_expr, true=result_expr)) + return self + + def else_(self, value: Any) -> "Case": + self.default = to_expression(value) + return self + + def end(self) -> "Case": + return self + + def as_(self, alias: str) -> exp.Alias: + return cast("exp.Alias", exp.alias_(self.expression, alias)) + + @property + def expression(self) -> exp.Case: + return exp.Case(ifs=self.conditions, default=self.default) + + +class CaseBuilder: + """Fluent builder for CASE expressions used within SELECT clauses.""" + + __slots__ = () + + def __call__(self, *args: Any, default: Any | None = None) -> Case: + conditions = [to_expression(arg) for arg in args] + default_expr = to_expression(default) if default is not None else None + return Case(*conditions, default=default_expr) diff --git a/sqlspec/builder/_delete.py b/sqlspec/builder/_delete.py index 3abc4c63e..509580364 100644 --- a/sqlspec/builder/_delete.py +++ b/sqlspec/builder/_delete.py @@ -8,9 +8,6 @@ from sqlglot import exp -if TYPE_CHECKING: - from sqlglot.dialects.dialect import DialectType - from sqlspec.builder._base import BuiltQuery, QueryBuilder from sqlspec.builder._dml import DeleteFromClauseMixin from sqlspec.builder._explain import ExplainMixin @@ -18,6 +15,9 @@ from sqlspec.core import SQLResult from sqlspec.exceptions import SQLBuilderError +if TYPE_CHECKING: + from sqlglot.dialects.dialect import DialectType + __all__ = ("Delete",) diff --git a/sqlspec/core/__init__.py b/sqlspec/core/__init__.py index c160396b6..c264055f1 100644 --- a/sqlspec/core/__init__.py +++ b/sqlspec/core/__init__.py @@ -210,6 +210,7 @@ ) from sqlspec.core.result import ( ArrowResult, + DMLResult, SQLResult, StackResult, StatementResult, @@ -267,6 +268,7 @@ "CompiledSQL", "ConditionFactory", "CorrelationExtractor", + "DMLResult", "DriverParameterProfile", "ExplainFormat", "ExplainOptions", diff --git a/sqlspec/core/_pool.py b/sqlspec/core/_pool.py index 07797bace..8abb4fdaf 100644 --- a/sqlspec/core/_pool.py +++ b/sqlspec/core/_pool.py @@ -40,10 +40,6 @@ def release(self, obj: T) -> None: self._pool.append(obj) -def _reset_noop(_: object) -> None: - return None - - def _create_sql() -> "SQL": from sqlspec.core.statement import SQL diff --git a/sqlspec/core/cache.py b/sqlspec/core/cache.py index 601247333..f11b1bbd6 100644 --- a/sqlspec/core/cache.py +++ b/sqlspec/core/cache.py @@ -12,6 +12,7 @@ import logging import threading import time +from collections.abc import Callable from typing import TYPE_CHECKING, Any, Final from mypy_extensions import mypyc_attr @@ -26,26 +27,32 @@ from sqlspec.utils.type_guards import has_field_name, has_filter_attributes if TYPE_CHECKING: - from collections.abc import Callable, Iterator + from collections.abc import Iterator import sqlglot.expressions as exp __all__ = ( + "CacheConfig", "CacheKey", "CacheStats", "CachedStatement", "FiltersView", "LRUCache", "NamespacedCache", + "clear_all_caches", "create_cache_key", "get_cache", "get_cache_config", "get_cache_instances", + "get_cache_statistics", "get_default_cache", "get_pipeline_metrics", + "log_cache_stats", "reset_pipeline_registry", + "reset_stats_only", "set_cache_instances", + "update_cache_config", ) logger = get_logger("sqlspec.cache") @@ -211,7 +218,7 @@ def __init__( namespace: Optional namespace identifier for logging context """ self._cache: dict[CacheKey, CacheNode] = {} - self._lock = threading.RLock() + self._lock = threading.Lock() self._max_size = max_size self._ttl = ttl_seconds self._stats = CacheStats() @@ -231,47 +238,54 @@ def get(self, key: CacheKey) -> Any | None: Returns: Cached value or None if not found or expired """ + debug_enabled = logger.isEnabledFor(logging.DEBUG) + log_event: str | None = None + log_reason: str | None = None + log_cache_size = 0 + result: Any | None = None + with self._lock: node = self._cache.get(key) if node is None: self._stats.record_miss() - if logger.isEnabledFor(logging.DEBUG): - log_with_context( - logger, - logging.DEBUG, - "cache.miss", - cache_namespace=self._namespace, - cache_size=len(self._cache), - ) - return None - - ttl = self._ttl - if ttl is not None: - current_time = time.time() - if (current_time - node.timestamp) > ttl: + if debug_enabled: + log_event = "cache.miss" + log_cache_size = len(self._cache) + else: + ttl = self._ttl + if ttl is not None and (time.time() - node.timestamp) > ttl: self._remove_node(node) del self._cache[key] self._stats.record_miss() self._stats.record_eviction() - if logger.isEnabledFor(logging.DEBUG): - log_with_context( - logger, - logging.DEBUG, - "cache.evict", - cache_namespace=self._namespace, - cache_size=len(self._cache), - reason="expired", - ) - return None - - self._move_to_head(node) - node.access_count += 1 - self._stats.record_hit() - if logger.isEnabledFor(logging.DEBUG): + if debug_enabled: + log_event = "cache.evict" + log_reason = "expired" + log_cache_size = len(self._cache) + else: + self._move_to_head(node) + node.access_count += 1 + self._stats.record_hit() + if debug_enabled: + log_event = "cache.hit" + log_cache_size = len(self._cache) + result = node.value + + if log_event is not None: + if log_reason is not None: + log_with_context( + logger, + logging.DEBUG, + log_event, + cache_namespace=self._namespace, + cache_size=log_cache_size, + reason=log_reason, + ) + else: log_with_context( - logger, logging.DEBUG, "cache.hit", cache_namespace=self._namespace, cache_size=len(self._cache) + logger, logging.DEBUG, log_event, cache_namespace=self._namespace, cache_size=log_cache_size ) - return node.value + return result def put(self, key: CacheKey, value: Any) -> None: """Put value in cache. @@ -623,7 +637,7 @@ def create_cache_key(namespace: str, key: str, dialect: str | None = None) -> st return f"{namespace}:{dialect or 'default'}:{key}" -NAMESPACED_CACHE_CONFIG: "dict[str, tuple[Callable[[CacheConfig], bool], Callable[[CacheConfig], int]]]" = { +NAMESPACED_CACHE_CONFIG: Final[dict[str, tuple[Callable[[CacheConfig], bool], Callable[[CacheConfig], int]]]] = { "statement": (lambda config: config.sql_cache_enabled, lambda config: config.sql_cache_size), "builder": (lambda config: config.sql_cache_enabled, lambda config: config.sql_cache_size), "expression": (lambda config: config.fragment_cache_enabled, lambda config: config.fragment_cache_size), @@ -743,6 +757,8 @@ def put_statement(self, key: str, value: Any, dialect: str | None = None) -> Non def delete_statement(self, key: str, dialect: str | None = None) -> bool: """Delete cached statement data. + External/extension API: not called internally. + Args: key: Cache key. dialect: Optional SQL dialect. @@ -777,6 +793,8 @@ def put_expression(self, key: str, value: Any, dialect: str | None = None) -> No def delete_expression(self, key: str, dialect: str | None = None) -> bool: """Delete cached expression data. + External/extension API: not called internally. + Args: key: Cache key. dialect: Optional SQL dialect. @@ -811,6 +829,8 @@ def put_optimized(self, key: str, value: Any, dialect: str | None = None) -> Non def delete_optimized(self, key: str, dialect: str | None = None) -> bool: """Delete cached optimized expression data. + External/extension API: not called internally. + Args: key: Cache key. dialect: Optional SQL dialect. @@ -845,6 +865,8 @@ def put_builder(self, key: str, value: Any, dialect: str | None = None) -> None: def delete_builder(self, key: str, dialect: str | None = None) -> bool: """Delete cached builder statement data. + External/extension API: not called internally. + Args: key: Cache key. dialect: Optional SQL dialect. @@ -879,6 +901,8 @@ def put_file(self, key: str, value: Any, dialect: str | None = None) -> None: def delete_file(self, key: str, dialect: str | None = None) -> bool: """Delete cached SQL file data. + External/extension API: not called internally. + Args: key: Cache key. dialect: Optional SQL dialect. @@ -981,6 +1005,8 @@ def __iter__(self) -> "Iterator[Any]": def get_by_field(self, field_name: str) -> "list[Any]": """Get all filters for a specific field. + External/extension API: not called internally. + Args: field_name: Field name to filter by @@ -992,6 +1018,8 @@ def get_by_field(self, field_name: str) -> "list[Any]": def has_field(self, field_name: str) -> bool: """Check if any filter exists for a field. + External/extension API: not called internally. + Args: field_name: Field name to check @@ -1003,6 +1031,8 @@ def has_field(self, field_name: str) -> bool: def to_canonical(self) -> "tuple[Any, ...]": """Create canonical representation for cache keys. + External/extension API: not called internally. + Returns: Canonical tuple representation of filters """ diff --git a/sqlspec/core/compiler.py b/sqlspec/core/compiler.py index 5331628d6..c21486a95 100644 --- a/sqlspec/core/compiler.py +++ b/sqlspec/core/compiler.py @@ -9,7 +9,7 @@ import logging from collections import OrderedDict from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Literal, final +from typing import TYPE_CHECKING, Any, Final, Literal, final import sqlglot from mypy_extensions import mypyc_attr @@ -59,7 +59,7 @@ "COMMAND", ] -OPERATION_TYPE_MAP: "dict[type[exp.Expr], OperationType]" = { +OPERATION_TYPE_MAP: Final[dict[type[exp.Expr], OperationType]] = { # Queries exp.Select: "SELECT", exp.Union: "SELECT", @@ -100,11 +100,11 @@ exp.Rollback: "COMMAND", } -COPY_OPERATION_TYPES: "tuple[OperationType, ...]" = ("COPY", "COPY_FROM", "COPY_TO") +COPY_OPERATION_TYPES: Final[tuple[OperationType, ...]] = ("COPY", "COPY_FROM", "COPY_TO") -COPY_FROM_OPERATION_TYPES: "tuple[OperationType, ...]" = ("COPY", "COPY_FROM") +COPY_FROM_OPERATION_TYPES: Final[tuple[OperationType, ...]] = ("COPY", "COPY_FROM") -COPY_TO_OPERATION_TYPES: "tuple[OperationType, ...]" = ("COPY_TO",) +COPY_TO_OPERATION_TYPES: Final[tuple[OperationType, ...]] = ("COPY_TO",) ParseCacheEntry = tuple[exp.Expr | None, OperationType, tuple[bool, bool]] @@ -280,11 +280,13 @@ class SQLProcessor: "_cache_misses", "_config", "_dialect_str", + "_enable_parameter_type_wrapping", "_exec_style", "_input_style", "_last_cache_key", "_last_result", "_max_cache_size", + "_parameter_config", "_parameter_processor", "_parse_cache", "_parse_cache_hits", @@ -312,6 +314,9 @@ def __init__( cache_enabled: Toggle compiled SQL caching (parse/parameter caches remain size-driven) """ self._config = config + parameter_config = config.parameter_config + self._parameter_config = parameter_config + self._enable_parameter_type_wrapping = config.enable_parameter_type_wrapping self._cache: OrderedDict[Any, CompiledSQL] = OrderedDict() self._max_cache_size = max(max_cache_size, 0) compiled_cache_active = cache_enabled and config.enable_caching and self._max_cache_size > 0 @@ -341,12 +346,9 @@ def __init__( # Pre-calculate static cache key components self._dialect_str = str(config.dialect) if config.dialect else None - self._input_style = config.parameter_config.default_parameter_style.value - self._exec_style = ( - config.parameter_config.default_execution_parameter_style.value - if config.parameter_config.default_execution_parameter_style - else self._input_style - ) + self._input_style = parameter_config.default_parameter_style.value + default_execution_style = parameter_config.default_execution_parameter_style + self._exec_style = default_execution_style.value if default_execution_style else self._input_style def compile( self, @@ -368,7 +370,7 @@ def compile( Returns: CompiledSQL with execution information """ - if not self._config.enable_caching or not self._cache_enabled: + if not self._cache_enabled: return self._compile_uncached(sql, parameters, is_many, expression, param_fingerprint=None) if param_fingerprint is None: @@ -403,7 +405,7 @@ def compile( def _materialize_cached_result(self, cached_result: CompiledSQL, parameters: Any, is_many: bool) -> CompiledSQL: """Return cached compilation metadata with parameters for the current call.""" - if self._config.parameter_config.needs_static_script_compilation: + if self._parameter_config.needs_static_script_compilation: return cached_result # Structural fingerprinting means same SQL structure = same cache entry, @@ -411,7 +413,7 @@ def _materialize_cached_result(self, cached_result: CompiledSQL, parameters: Any processed_params = self._parameter_processor._transform_cached_parameters( # pyright: ignore[reportPrivateUsage] parameters, cached_result.parameter_profile, - self._config.parameter_config, + self._parameter_config, input_named_parameters=cached_result.input_named_parameters, is_many=is_many, apply_wrap_types=cached_result.applied_wrap_types, @@ -455,10 +457,10 @@ def _prepare_parameters( process_result = self._parameter_processor.process( sql=sql, parameters=parameters, - config=self._config.parameter_config, + config=self._parameter_config, dialect=dialect_str, is_many=is_many, - wrap_types=self._config.enable_parameter_type_wrapping, + wrap_types=self._enable_parameter_type_wrapping, param_fingerprint=param_fingerprint, ) return ( @@ -573,7 +575,7 @@ def _resolve_expression( """ parse_cache_key = None parse_cache_entry = None - if self._config.enable_caching and self._parse_cache_max_size > 0: + if self._parse_cache_max_size > 0: parse_cache_key = SQLProcessor._make_parse_cache_key(sqlglot_sql, dialect_str) parse_cache_entry = self._parse_cache.get(parse_cache_key) if parse_cache_entry is not None: @@ -620,7 +622,7 @@ def _apply_ast_transformers( Updated expression metadata and transformation state. """ statement_transformers = self._config.statement_transformers - ast_transformer = self._config.parameter_config.ast_transformer + ast_transformer = self._parameter_config.ast_transformer if expression is None or (not statement_transformers and not ast_transformer): return expression, parameters, False, operation_type, operation_profile @@ -682,17 +684,17 @@ def _finalize_compilation( Final SQL, execution parameters, parameter profile, input named parameters, and applied wrap types flag. """ - if self._config.parameter_config.needs_static_script_compilation and processed_params is None: + if self._parameter_config.needs_static_script_compilation and processed_params is None: return processed_sql, processed_params, parameter_profile, (), False if ast_was_transformed and expression is not None: # Pass the transformed expression through the pipeline to avoid re-parsing transformed_result = self._parameter_processor.process_for_execution( sql=expression.sql(dialect=dialect_str), parameters=parameters, - config=self._config.parameter_config, + config=self._parameter_config, dialect=dialect_str, is_many=is_many, - wrap_types=self._config.enable_parameter_type_wrapping, + wrap_types=self._enable_parameter_type_wrapping, parsed_expression=expression, ) final_sql = transformed_result.sql @@ -732,7 +734,8 @@ def _should_validate_parameters(self, final_params: Any, raw_parameters: Any, is has_params = has_final_params or has_raw_params return (has_params or is_many) and validation_enabled - def _validate_parameters(self, parameter_profile: "ParameterProfile", final_params: Any, is_many: bool) -> None: + @staticmethod + def _validate_parameters(parameter_profile: "ParameterProfile", final_params: Any, is_many: bool) -> None: """Validate parameter alignment and log failures. Args: @@ -836,7 +839,7 @@ def _compile_uncached( execution_parameters=final_params, operation_type=operation_type, expression=expression, - parameter_style=self._config.parameter_config.default_parameter_style.value, + parameter_style=self._input_style, supports_many=isinstance(final_params, list) and len(final_params) > 0, parameter_casts=parameter_casts, parameter_profile=parameter_profile, @@ -862,7 +865,7 @@ def _compile_uncached( raise def _get_param_fingerprint(self, parameters: Any, is_many: bool) -> Any: - if self._config.parameter_config.needs_static_script_compilation: + if self._parameter_config.needs_static_script_compilation: return value_fingerprint(parameters) return structural_fingerprint(parameters, is_many) diff --git a/sqlspec/core/config_runtime.py b/sqlspec/core/config_runtime.py index f9463ca24..c28414b10 100644 --- a/sqlspec/core/config_runtime.py +++ b/sqlspec/core/config_runtime.py @@ -5,7 +5,8 @@ import threading from typing import TYPE_CHECKING, Any, TypeVar -from sqlspec.core import ParameterStyle, ParameterStyleConfig, StatementConfig +from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig +from sqlspec.core.statement import StatementConfig if TYPE_CHECKING: from collections.abc import Awaitable, Callable diff --git a/sqlspec/core/explain.py b/sqlspec/core/explain.py index 2142f862d..aa09a94ae 100644 --- a/sqlspec/core/explain.py +++ b/sqlspec/core/explain.py @@ -12,7 +12,7 @@ __all__ = ("ExplainFormat", "ExplainOptions") -EXPLAIN_OPTIONS_SLOTS: Final = ( +EXPLAIN_OPTION_FIELDS: Final[tuple[str, ...]] = ( "analyze", "verbose", "format", @@ -25,6 +25,7 @@ "wal", "generic_plan", ) +EXPLAIN_OPTIONS_SLOTS: Final = EXPLAIN_OPTION_FIELDS class ExplainFormat(str, Enum): @@ -139,63 +140,28 @@ def __init__( def __repr__(self) -> str: """String representation of ExplainOptions.""" parts = [] - if self.analyze: - parts.append("analyze=True") - if self.verbose: - parts.append("verbose=True") - if self.format is not None: - parts.append(f"format={self.format.value!r}") - if self.costs is not None: - parts.append(f"costs={self.costs}") - if self.buffers is not None: - parts.append(f"buffers={self.buffers}") - if self.timing is not None: - parts.append(f"timing={self.timing}") - if self.summary is not None: - parts.append(f"summary={self.summary}") - if self.memory is not None: - parts.append(f"memory={self.memory}") - if self.settings is not None: - parts.append(f"settings={self.settings}") - if self.wal is not None: - parts.append(f"wal={self.wal}") - if self.generic_plan is not None: - parts.append(f"generic_plan={self.generic_plan}") + for field_name in EXPLAIN_OPTION_FIELDS: + value = self._field_value(field_name) + if field_name in ("analyze", "verbose"): + if value is not True: + continue + elif value is None: + continue + if isinstance(value, ExplainFormat): + parts.append(f"{field_name}={value.value!r}") + else: + parts.append(f"{field_name}={value}") return f"ExplainOptions({', '.join(parts)})" def __eq__(self, other: object) -> bool: """Equality comparison.""" if not isinstance(other, ExplainOptions): return False - return ( - self.analyze == other.analyze - and self.verbose == other.verbose - and self.format == other.format - and self.costs == other.costs - and self.buffers == other.buffers - and self.timing == other.timing - and self.summary == other.summary - and self.memory == other.memory - and self.settings == other.settings - and self.wal == other.wal - and self.generic_plan == other.generic_plan - ) + return self._key() == other._key() def __hash__(self) -> int: """Hash computation.""" - return hash(( - self.analyze, - self.verbose, - self.format, - self.costs, - self.buffers, - self.timing, - self.summary, - self.memory, - self.settings, - self.wal, - self.generic_plan, - )) + return hash(self._key()) def copy( self, @@ -250,26 +216,40 @@ def to_dict(self) -> "dict[str, Any]": Dictionary of option names to values """ result: dict[str, Any] = {} - if self.analyze: - result["analyze"] = True - if self.verbose: - result["verbose"] = True - if self.format is not None: - result["format"] = self.format.value.upper() - if self.costs is not None: - result["costs"] = self.costs - if self.buffers is not None: - result["buffers"] = self.buffers - if self.timing is not None: - result["timing"] = self.timing - if self.summary is not None: - result["summary"] = self.summary - if self.memory is not None: - result["memory"] = self.memory - if self.settings is not None: - result["settings"] = self.settings - if self.wal is not None: - result["wal"] = self.wal - if self.generic_plan is not None: - result["generic_plan"] = self.generic_plan + for field_name in EXPLAIN_OPTION_FIELDS: + value = self._field_value(field_name) + if field_name in ("analyze", "verbose"): + if value is not True: + continue + elif value is None: + continue + result[field_name] = value.value.upper() if isinstance(value, ExplainFormat) else value return result + + def _key(self) -> "tuple[Any, ...]": + return tuple(self._field_value(field_name) for field_name in EXPLAIN_OPTION_FIELDS) + + def _field_value(self, field_name: str) -> Any: + if field_name == "analyze": + return self.analyze + if field_name == "verbose": + return self.verbose + if field_name == "format": + return self.format + if field_name == "costs": + return self.costs + if field_name == "buffers": + return self.buffers + if field_name == "timing": + return self.timing + if field_name == "summary": + return self.summary + if field_name == "memory": + return self.memory + if field_name == "settings": + return self.settings + if field_name == "wal": + return self.wal + if field_name == "generic_plan": + return self.generic_plan + return None diff --git a/sqlspec/core/filters.py b/sqlspec/core/filters.py index b98a4eec2..f110900fd 100644 --- a/sqlspec/core/filters.py +++ b/sqlspec/core/filters.py @@ -107,8 +107,10 @@ def _resolve_parameter_conflicts(self, statement: "SQL", proposed_names: "list[s Returns: List of resolved parameter names (same length as proposed_names) """ - existing_params = set(statement.named_parameters.keys()) - existing_params.update(statement.parameters.keys() if isinstance(statement.parameters, dict) else []) + parameters = statement.parameters + existing_params = ( + set(parameters.keys()) if isinstance(parameters, dict) else set(statement.named_parameters.keys()) + ) resolved_names = [] for name in proposed_names: @@ -143,7 +145,6 @@ def _sanitize_param_name(self, name: "str | exp.Expression") -> str: str: Sanitized parameter name """ if isinstance(name, exp.Expression): - # For expressions, we use a hash or a generic name since we can't easily sanitize return f"expr_{str(hash(name)).replace('-', '')[:8]}" return name.replace(".", "_") @@ -183,41 +184,39 @@ def get_cache_key(self) -> "tuple[Any, ...]": """ -class BeforeAfterFilter(StatementFilter): - """Filter for datetime range queries. +class _DatetimeBoundFilter(StatementFilter): + """Private base for datetime lower/upper-bound filters.""" - Applies WHERE clauses for before/after datetime filtering. - """ + __slots__ = ("_field_name", "_lower_value", "_upper_value") - __slots__ = ("_after", "_before", "_field_name") + _cache_key_name: ClassVar[str] = "_DatetimeBoundFilter" + _lower_condition: ClassVar["type[Condition]"] = exp.GT + _lower_param_suffix: ClassVar[str] = "after" + _upper_condition: ClassVar["type[Condition]"] = exp.LT + _upper_param_suffix: ClassVar[str] = "before" def __init__( - self, field_name: "str | exp.Expression", before: datetime | None = None, after: datetime | None = None + self, + field_name: "str | exp.Expression", + upper_value: datetime | None = None, + lower_value: datetime | None = None, ) -> None: self._field_name = field_name - self._before = before - self._after = after + self._upper_value = upper_value + self._lower_value = lower_value @property def field_name(self) -> "str | exp.Expression": return self._field_name - @property - def before(self) -> datetime | None: - return self._before - - @property - def after(self) -> datetime | None: - return self._after - def get_param_names(self) -> "list[str]": """Get parameter names without storing them.""" names = [] sanitized_field = self._sanitize_param_name(self.field_name) - if self.before: - names.append(f"{sanitized_field}_before") - if self.after: - names.append(f"{sanitized_field}_after") + if self._upper_value: + names.append(f"{sanitized_field}_{self._upper_param_suffix}") + if self._lower_value: + names.append(f"{sanitized_field}_{self._lower_param_suffix}") return names def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]": @@ -225,11 +224,11 @@ def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]": named_parameters = {} param_names = self.get_param_names() param_idx = 0 - if self.before: - named_parameters[param_names[param_idx]] = self.before + if self._upper_value: + named_parameters[param_names[param_idx]] = self._upper_value param_idx += 1 - if self.after: - named_parameters[param_names[param_idx]] = self.after + if self._lower_value: + named_parameters[param_names[param_idx]] = self._lower_value return [], named_parameters def append_to_statement(self, statement: "SQL") -> "SQL": @@ -244,25 +243,24 @@ def append_to_statement(self, statement: "SQL") -> "SQL": param_idx = 0 result = statement - if self.before: - before_param_name = resolved_names[param_idx] + if self._upper_value: + upper_param_name = resolved_names[param_idx] param_idx += 1 conditions.append( - exp.LT( - this=self._get_column_expression(self.field_name), - expression=exp.Placeholder(this=before_param_name), + self._upper_condition( + this=self._get_column_expression(self.field_name), expression=exp.Placeholder(this=upper_param_name) ) ) - result = result.add_named_parameter(before_param_name, self.before) + result = result.add_named_parameter(upper_param_name, self._upper_value) - if self.after: - after_param_name = resolved_names[param_idx] + if self._lower_value: + lower_param_name = resolved_names[param_idx] conditions.append( - exp.GT( - this=self._get_column_expression(self.field_name), expression=exp.Placeholder(this=after_param_name) + self._lower_condition( + this=self._get_column_expression(self.field_name), expression=exp.Placeholder(this=lower_param_name) ) ) - result = result.add_named_parameter(after_param_name, self.after) + result = result.add_named_parameter(lower_param_name, self._lower_value) final_condition = conditions[0] for cond in conditions[1:]: @@ -271,106 +269,69 @@ def append_to_statement(self, statement: "SQL") -> "SQL": def get_cache_key(self) -> "tuple[Any, ...]": """Return cache key for this filter configuration.""" - return ("BeforeAfterFilter", self.field_name, self.before, self.after) + return (self._cache_key_name, self.field_name, self._upper_value, self._lower_value) def _reconstruction_args(self) -> "tuple[Any, ...]": - return (self._field_name, self._before, self._after) + return (self._field_name, self._upper_value, self._lower_value) -class OnBeforeAfterFilter(StatementFilter): - """Filter for inclusive datetime range queries. +class BeforeAfterFilter(_DatetimeBoundFilter): + """Filter for datetime range queries. - Applies WHERE clauses for on-or-before/on-or-after datetime filtering. + Applies WHERE clauses for before/after datetime filtering. """ - __slots__ = ("_field_name", "_on_or_after", "_on_or_before") + __slots__ = () + + _cache_key_name: ClassVar[str] = "BeforeAfterFilter" + _lower_condition: ClassVar["type[Condition]"] = exp.GT + _lower_param_suffix: ClassVar[str] = "after" + _upper_condition: ClassVar["type[Condition]"] = exp.LT + _upper_param_suffix: ClassVar[str] = "before" def __init__( - self, - field_name: "str | exp.Expression", - on_or_before: datetime | None = None, - on_or_after: datetime | None = None, + self, field_name: "str | exp.Expression", before: datetime | None = None, after: datetime | None = None ) -> None: - self._field_name = field_name - self._on_or_before = on_or_before - self._on_or_after = on_or_after - - @property - def field_name(self) -> "str | exp.Expression": - return self._field_name + super().__init__(field_name, before, after) @property - def on_or_before(self) -> datetime | None: - return self._on_or_before + def before(self) -> datetime | None: + return self._upper_value @property - def on_or_after(self) -> datetime | None: - return self._on_or_after - - def get_param_names(self) -> "list[str]": - """Get parameter names without storing them.""" - names = [] - sanitized_field = self._sanitize_param_name(self.field_name) - if self.on_or_before: - names.append(f"{sanitized_field}_on_or_before") - if self.on_or_after: - names.append(f"{sanitized_field}_on_or_after") - return names - - def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]": - """Extract filter parameters.""" - named_parameters = {} - param_names = self.get_param_names() - param_idx = 0 - if self.on_or_before: - named_parameters[param_names[param_idx]] = self.on_or_before - param_idx += 1 - if self.on_or_after: - named_parameters[param_names[param_idx]] = self.on_or_after - return [], named_parameters + def after(self) -> datetime | None: + return self._lower_value - def append_to_statement(self, statement: "SQL") -> "SQL": - conditions: list[Condition] = [] - proposed_names = self.get_param_names() - if not proposed_names: - return statement +class OnBeforeAfterFilter(_DatetimeBoundFilter): + """Filter for inclusive datetime range queries. - resolved_names = self._resolve_parameter_conflicts(statement, proposed_names) + Applies WHERE clauses for on-or-before/on-or-after datetime filtering. + """ - param_idx = 0 - result = statement - if self.on_or_before: - before_param_name = resolved_names[param_idx] - param_idx += 1 - conditions.append( - exp.LTE( - this=self._get_column_expression(self.field_name), - expression=exp.Placeholder(this=before_param_name), - ) - ) - result = result.add_named_parameter(before_param_name, self.on_or_before) + __slots__ = () - if self.on_or_after: - after_param_name = resolved_names[param_idx] - conditions.append( - exp.GTE( - this=self._get_column_expression(self.field_name), expression=exp.Placeholder(this=after_param_name) - ) - ) - result = result.add_named_parameter(after_param_name, self.on_or_after) + _cache_key_name: ClassVar[str] = "OnBeforeAfterFilter" + _lower_condition: ClassVar["type[Condition]"] = exp.GTE + _lower_param_suffix: ClassVar[str] = "on_or_after" + _upper_condition: ClassVar["type[Condition]"] = exp.LTE + _upper_param_suffix: ClassVar[str] = "on_or_before" - final_condition = conditions[0] - for cond in conditions[1:]: - final_condition = exp.And(this=final_condition, expression=cond) - return result.where(final_condition) + def __init__( + self, + field_name: "str | exp.Expression", + on_or_before: datetime | None = None, + on_or_after: datetime | None = None, + ) -> None: + super().__init__(field_name, on_or_before, on_or_after) - def get_cache_key(self) -> "tuple[Any, ...]": - """Return cache key for this filter configuration.""" - return ("OnBeforeAfterFilter", self.field_name, self.on_or_before, self.on_or_after) + @property + def on_or_before(self) -> datetime | None: + return self._upper_value - def _reconstruction_args(self) -> "tuple[Any, ...]": - return (self._field_name, self._on_or_before, self._on_or_after) + @property + def on_or_after(self) -> datetime | None: + return self._lower_value class InAnyFilter(StatementFilter, ABC, Generic[T]): @@ -627,14 +588,17 @@ def _reconstruction_args(self) -> "tuple[Any, ...]": return (self._field_name, self._sort_order) -class SearchFilter(StatementFilter): - """Filter for text search queries. - - Constructs WHERE field_name LIKE '%value%' clauses. - """ +class _TextSearchFilter(StatementFilter): + """Private base for LIKE and NOT LIKE filters.""" __slots__ = ("_field_name", "_ignore_case", "_value") + _cache_key_name: ClassVar[str] = "SearchFilter" + _combine_operator: ClassVar[Literal["and", "or"]] = "or" + _negate: ClassVar[bool] = False + _param_suffix: ClassVar[str] = "search" + _set_param_name: ClassVar[str] = "search_value" + def __init__( self, field_name: "str | exp.Expression | set[str | exp.Expression]", @@ -688,10 +652,10 @@ def get_param_name(self) -> str | None: return None if isinstance(self.field_name, str): sanitized_field = self._sanitize_param_name(self.field_name) - return f"{sanitized_field}_search" + return f"{sanitized_field}_{self._param_suffix}" if isinstance(self.field_name, exp.Expression): - return f"{self._sanitize_param_name(self.field_name)}_search" - return "search_value" + return f"{self._sanitize_param_name(self.field_name)}_{self._param_suffix}" + return self._set_param_name def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]": """Extract filter parameters.""" @@ -709,31 +673,23 @@ def append_to_statement(self, statement: "SQL") -> "SQL": resolved_names = self._resolve_parameter_conflicts(statement, [param_name]) param_name = resolved_names[0] - like_op = exp.ILike if self.ignore_case else exp.Like - - if isinstance(self.field_name, str): - col_expr = self._get_column_expression(self.field_name) - result = statement.where(like_op(this=col_expr, expression=exp.Placeholder(this=param_name))) - elif isinstance(self.field_name, exp.Expression): - result = statement.where(like_op(this=self.field_name, expression=exp.Placeholder(this=param_name))) + if isinstance(self.field_name, (str, exp.Expression)): + condition = self._build_search_condition(self.field_name, param_name) + result = statement.where(condition) elif isinstance(self.field_name, set) and self.field_name: - # do not hoist Placeholder outside this comprehension; sqlglot nodes carry parent pointers. - field_conditions: list[Condition] = [ - like_op(this=self._get_column_expression(field), expression=exp.Placeholder(this=param_name)) - for field in self.field_name - ] - if not field_conditions: - return statement - + field_conditions = [self._build_search_condition(field, param_name) for field in self.field_name] final_condition: Condition = field_conditions[0] for cond in field_conditions[1:]: - final_condition = exp.Or(this=final_condition, expression=cond) + if self._combine_operator == "and": + final_condition = exp.And(this=final_condition, expression=cond) + else: + final_condition = exp.Or(this=final_condition, expression=cond) result = statement.where(final_condition) elif isinstance(self.field_name, set): return statement else: msg = ( - f"SearchFilter.field_name must be str, exp.Expression, or set thereof; " + f"{type(self).__name__}.field_name must be str, exp.Expression, or set thereof; " f"got {type(self.field_name).__name__}" ) raise TypeError(msg) @@ -750,12 +706,50 @@ def get_cache_key(self) -> "tuple[Any, ...]": field_names = self.field_name.sql() else: field_names = self.field_name - return ("SearchFilter", field_names, self.value, self.ignore_case) + return (self._cache_key_name, field_names, self.value, self.ignore_case) + + def _build_search_condition(self, field_name: "str | exp.Expression", param_name: str) -> "Condition": + like_op = exp.ILike if self.ignore_case else exp.Like + search_target = ( + field_name if isinstance(field_name, exp.Expression) else self._get_column_expression(field_name) + ) + condition = like_op(this=search_target, expression=exp.Placeholder(this=param_name)) + return exp.Not(this=condition) if self._negate else condition def _reconstruction_args(self) -> "tuple[Any, ...]": return (self._field_name, self._value, self._ignore_case) +class SearchFilter(_TextSearchFilter): + """Filter for text search queries. + + Constructs WHERE field_name LIKE '%value%' clauses. + """ + + __slots__ = () + + _cache_key_name: ClassVar[str] = "SearchFilter" + _combine_operator: ClassVar[Literal["and", "or"]] = "or" + _negate: ClassVar[bool] = False + _param_suffix: ClassVar[str] = "search" + _set_param_name: ClassVar[str] = "search_value" + + +class NotInSearchFilter(SearchFilter): + """Filter for negated text search queries. + + Constructs WHERE field_name NOT LIKE '%value%' clauses. + """ + + __slots__ = () + + _cache_key_name: ClassVar[str] = "NotInSearchFilter" + _combine_operator: ClassVar[Literal["and", "or"]] = "and" + _negate: ClassVar[bool] = True + _param_suffix: ClassVar[str] = "not_search" + _set_param_name: ClassVar[str] = "not_search_value" + + class NullFilter(StatementFilter): """Filter for IS NULL queries. @@ -877,90 +871,6 @@ def _reconstruction_args(self) -> "tuple[Any, ...]": return (self._field_name, self._value) -class NotInSearchFilter(SearchFilter): - """Filter for negated text search queries. - - Constructs WHERE field_name NOT LIKE '%value%' clauses. - """ - - __slots__ = () - - def get_param_name(self) -> str | None: - """Get parameter name without storing it.""" - if not self.value: - return None - if isinstance(self.field_name, str): - sanitized_field = self._sanitize_param_name(self.field_name) - return f"{sanitized_field}_not_search" - if isinstance(self.field_name, exp.Expression): - return f"{self._sanitize_param_name(self.field_name)}_not_search" - return "not_search_value" - - def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]": - """Extract filter parameters.""" - named_parameters = {} - param_name = self.get_param_name() - if self.value and param_name: - named_parameters[param_name] = self.like_pattern - return [], named_parameters - - def append_to_statement(self, statement: "SQL") -> "SQL": - param_name = self.get_param_name() - if not self.value or not param_name: - return statement - - resolved_names = self._resolve_parameter_conflicts(statement, [param_name]) - param_name = resolved_names[0] - - like_op = exp.ILike if self.ignore_case else exp.Like - - if isinstance(self.field_name, str): - col_expr = self._get_column_expression(self.field_name) - result = statement.where(exp.Not(this=like_op(this=col_expr, expression=exp.Placeholder(this=param_name)))) - elif isinstance(self.field_name, exp.Expression): - result = statement.where( - exp.Not(this=like_op(this=self.field_name, expression=exp.Placeholder(this=param_name))) - ) - elif isinstance(self.field_name, set) and self.field_name: - # do not hoist Placeholder outside this comprehension; sqlglot nodes carry parent pointers. - field_conditions: list[Condition] = [ - exp.Not( - this=like_op(this=self._get_column_expression(field), expression=exp.Placeholder(this=param_name)) - ) - for field in self.field_name - ] - if not field_conditions: - return statement - - final_condition: Condition = field_conditions[0] - if len(field_conditions) > 1: - for cond in field_conditions[1:]: - final_condition = exp.And(this=final_condition, expression=cond) - result = statement.where(final_condition) - elif isinstance(self.field_name, set): - return statement - else: - msg = ( - f"NotInSearchFilter.field_name must be str, exp.Expression, or set thereof; " - f"got {type(self.field_name).__name__}" - ) - raise TypeError(msg) - - return result.add_named_parameter(param_name, self.like_pattern) - - def get_cache_key(self) -> "tuple[Any, ...]": - """Return cache key for this filter configuration.""" - if isinstance(self.field_name, set): - field_names: Any = tuple( - sorted(item.sql() if isinstance(item, exp.Expression) else item for item in self.field_name) - ) - elif isinstance(self.field_name, exp.Expression): - field_names = self.field_name.sql() - else: - field_names = self.field_name - return ("NotInSearchFilter", field_names, self.value, self.ignore_case) - - def apply_filter(statement: "SQL", filter_obj: StatementFilter) -> "SQL": """Apply a statement filter to a SQL query object. diff --git a/sqlspec/core/hashing.py b/sqlspec/core/hashing.py index 3e8930536..b2a95bb32 100644 --- a/sqlspec/core/hashing.py +++ b/sqlspec/core/hashing.py @@ -9,7 +9,7 @@ from sqlglot import exp from sqlspec.core.parameters import TypedParameter -from sqlspec.utils.type_guards import is_expression, is_typed_parameter +from sqlspec.utils.type_guards import is_expression if TYPE_CHECKING: from collections.abc import Sequence @@ -20,6 +20,7 @@ __all__ = ( "hash_expression", "hash_expression_node", + "hash_filters", "hash_optimized_expression", "hash_parameters", "hash_sql_statement", @@ -121,7 +122,7 @@ def hash_parameters( if named_parameters: hashable_items: list[tuple[str, tuple[Any, Any]]] = [] for key, value in sorted(named_parameters.items()): - if is_typed_parameter(value): + if isinstance(value, TypedParameter): if isinstance(value.value, (list, dict)): hashable_items.append((key, (repr(value.value), value.original_type))) else: @@ -149,13 +150,6 @@ def hash_parameters( return param_hash -def _hash_filter_value(value: Any) -> int: - try: - return hash(value) - except TypeError: - return hash(repr(value)) - - def hash_filters(filters: "Sequence[StatementFilter] | None" = None) -> int: """Generate hash for statement filters. diff --git a/sqlspec/core/parameters/_alignment.py b/sqlspec/core/parameters/_alignment.py index c12be8f47..1016d99bd 100644 --- a/sqlspec/core/parameters/_alignment.py +++ b/sqlspec/core/parameters/_alignment.py @@ -1,7 +1,7 @@ """Parameter alignment and validation helpers.""" from collections.abc import Mapping, Sequence -from typing import Any, cast +from typing import Any, Final, cast import sqlspec.exceptions from sqlspec.core.parameters._types import _NAMED_STYLES, ParameterProfile, ParameterStyle @@ -14,7 +14,7 @@ "validate_parameter_alignment", ) -EXECUTE_MANY_MIN_ROWS: int = 2 +EXECUTE_MANY_MIN_ROWS: Final[int] = 2 def normalize_parameter_key(key: Any) -> "tuple[str, int | str]": @@ -130,8 +130,19 @@ def validate_parameter_alignment( raise sqlspec.exceptions.SQLSpecError(msg) if len(parameters) == 0: return + expected_identifiers = _collect_expected_identifiers(profile) + expected_count = len(expected_identifiers) + named_identifier_aliases = _collect_named_identifier_aliases(profile) for index, param_set in enumerate(parameters): - _validate_single_parameter_set(profile, param_set, batch_index=index) + actual_identifiers, actual_count = _collect_actual_identifiers(param_set) + _validate_parameter_identifiers( + expected_identifiers, + expected_count, + actual_identifiers, + actual_count, + named_identifier_aliases, + batch_index=index, + ) return _validate_single_parameter_set(profile, parameters) @@ -189,12 +200,21 @@ def _collect_actual_identifiers(parameters: Any) -> "tuple[set[tuple[str, int | def _normalize_named_identifier_aliases( parameter_profile: "ParameterProfile", identifiers: "set[tuple[str, int | str]]" ) -> "set[tuple[str, int | str]]": + return _apply_named_identifier_aliases(identifiers, _collect_named_identifier_aliases(parameter_profile)) + + +def _collect_named_identifier_aliases(parameter_profile: "ParameterProfile") -> "dict[str, str]": aliases: dict[str, str] = {} for parameter in parameter_profile.parameters: if parameter.style not in _NAMED_STYLES or not parameter.name: continue aliases[parameter.placeholder_text] = parameter.name + return aliases + +def _apply_named_identifier_aliases( + identifiers: "set[tuple[str, int | str]]", aliases: "dict[str, str]" +) -> "set[tuple[str, int | str]]": if not aliases: return identifiers @@ -263,7 +283,24 @@ def _validate_single_parameter_set( expected_identifiers = _collect_expected_identifiers(parameter_profile) actual_identifiers, actual_count = _collect_actual_identifiers(parameters) expected_count = len(expected_identifiers) + _validate_parameter_identifiers( + expected_identifiers, + expected_count, + actual_identifiers, + actual_count, + _collect_named_identifier_aliases(parameter_profile), + batch_index=batch_index, + ) + +def _validate_parameter_identifiers( + expected_identifiers: "set[tuple[str, int | str]]", + expected_count: int, + actual_identifiers: "set[tuple[str, int | str]]", + actual_count: int, + named_identifier_aliases: "dict[str, str]", + batch_index: "int | None" = None, +) -> None: if expected_count == 0 and actual_count == 0: return @@ -283,7 +320,7 @@ def _validate_single_parameter_set( msg = f"{prefix}: {actual_count} parameters provided but {expected_count} placeholders detected." raise sqlspec.exceptions.SQLSpecError(msg) - normalized_actual_identifiers = _normalize_named_identifier_aliases(parameter_profile, actual_identifiers) + normalized_actual_identifiers = _apply_named_identifier_aliases(actual_identifiers, named_identifier_aliases) identifiers_match = expected_identifiers == normalized_actual_identifiers or _normalize_index_identifiers( expected_identifiers, normalized_actual_identifiers ) diff --git a/sqlspec/core/parameters/_converter.py b/sqlspec/core/parameters/_converter.py index 5da84ed6d..1076b9288 100644 --- a/sqlspec/core/parameters/_converter.py +++ b/sqlspec/core/parameters/_converter.py @@ -1,12 +1,13 @@ """Parameter style conversion utilities.""" from collections.abc import Callable, Mapping, Sequence -from typing import Any +from typing import Any, Final from mypy_extensions import mypyc_attr from sqlspec.core.parameters._types import ( _NAMED_STYLES, + _POSITIONAL_STYLES, ConvertedParameters, NamedParameterOutput, ParameterInfo, @@ -22,6 +23,15 @@ __all__ = ("ParameterConverter",) _ORDERED_PARAM_INFO_MIN_SIZE = 2 +_OCCURRENCE_KEYED_STYLES: Final[frozenset[ParameterStyle]] = frozenset({ + ParameterStyle.QMARK, + ParameterStyle.POSITIONAL_PYFORMAT, +}) +_EXPANDING_POSITIONAL_STYLES: Final[frozenset[ParameterStyle]] = frozenset({ + ParameterStyle.QMARK, + ParameterStyle.POSITIONAL_PYFORMAT, + ParameterStyle.POSITIONAL_COLON, +}) def _placeholder_qmark(_: Any) -> str: @@ -80,16 +90,11 @@ def _single_parameter_style(param_info: "list[ParameterInfo]") -> "ParameterStyl def _is_positional_style(style: "ParameterStyle") -> bool: - return style in { - ParameterStyle.QMARK, - ParameterStyle.NUMERIC, - ParameterStyle.POSITIONAL_PYFORMAT, - ParameterStyle.POSITIONAL_COLON, - } + return style in _POSITIONAL_STYLES def _parameter_lookup_key(param: "ParameterInfo") -> str: - if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}: + if param.style in _OCCURRENCE_KEYED_STYLES: return f"{param.placeholder_text}_{param.ordinal}" return param.placeholder_text @@ -192,7 +197,6 @@ def _convert_placeholders_to_style( else: ordered_params, unique_params = self._build_conversion_plan(param_info, target_style) - placeholder_text_len_cache: dict[str, int] = {} # Build SQL using forward iteration with list join (O(n) vs O(n^2) string slicing) segments: list[str] = [] last_end = 0 @@ -200,11 +204,6 @@ def _convert_placeholders_to_style( is_positional_style = _is_positional_style(target_style) for param in ordered_params: - # Cache placeholder text length - if param.placeholder_text not in placeholder_text_len_cache: - placeholder_text_len_cache[param.placeholder_text] = len(param.placeholder_text) - text_len = placeholder_text_len_cache[param.placeholder_text] - # Generate new placeholder based on target style if is_positional_style: param_key = _parameter_lookup_key(param) @@ -215,7 +214,7 @@ def _convert_placeholders_to_style( # Append segment before this placeholder and the new placeholder segments.extend((sql[last_end : param.position], new_placeholder)) - last_end = param.position + text_len + last_end = param.position + len(param.placeholder_text) # Append remaining SQL after last placeholder segments.append(sql[last_end:]) @@ -412,12 +411,7 @@ def _convert_parameter_format( return tuple(normalized_sets) return normalized_sets - is_named_style = target_style in { - ParameterStyle.NAMED_COLON, - ParameterStyle.NAMED_AT, - ParameterStyle.NAMED_DOLLAR, - ParameterStyle.NAMED_PYFORMAT, - } + is_named_style = target_style in _NAMED_STYLES if is_named_style: if isinstance(parameters, Mapping): return self._align_mapping_for_named_style(parameters, param_info) @@ -458,11 +452,7 @@ def _convert_parameter_format( unique_params[param_key] = value param_order.append(param_key) - needs_expansion = target_style in { - ParameterStyle.QMARK, - ParameterStyle.POSITIONAL_PYFORMAT, - ParameterStyle.POSITIONAL_COLON, - } + needs_expansion = target_style in _EXPANDING_POSITIONAL_STYLES if needs_expansion: param_values = [] @@ -489,7 +479,7 @@ def _embed_static_parameters( unique_params: dict[str, int] = {} for param in param_info: - if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}: + if param.style in _OCCURRENCE_KEYED_STYLES: param_key = f"{param.placeholder_text}_{param.ordinal}" elif (param.style == ParameterStyle.NUMERIC and param.name) or param.name: param_key = param.placeholder_text @@ -521,24 +511,10 @@ def _embed_static_parameters( return static_sql, None - def _get_parameter_value(self, parameters: "ParameterPayload", param: "ParameterInfo") -> object | None: - if isinstance(parameters, Mapping): - if param.name and param.name in parameters: - return parameters[param.name] - if f"param_{param.ordinal}" in parameters: - return parameters[f"param_{param.ordinal}"] - if str(param.ordinal + 1) in parameters: - return parameters[str(param.ordinal + 1)] - elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): - if param.ordinal < len(parameters): - return parameters[param.ordinal] - - return None - def _get_parameter_value_with_reuse( self, parameters: "ParameterPayload", param: "ParameterInfo", unique_params: "dict[str, int]" ) -> object | None: - if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}: + if param.style in _OCCURRENCE_KEYED_STYLES: param_key = f"{param.placeholder_text}_{param.ordinal}" elif (param.style == ParameterStyle.NUMERIC and param.name) or param.name: param_key = param.placeholder_text diff --git a/sqlspec/core/parameters/_declared.py b/sqlspec/core/parameters/_declared.py index 91cb543f3..218be8369 100644 --- a/sqlspec/core/parameters/_declared.py +++ b/sqlspec/core/parameters/_declared.py @@ -9,7 +9,7 @@ from collections.abc import Callable from datetime import date, datetime, time from decimal import Decimal -from typing import TypeAlias +from typing import Final, TypeAlias from uuid import UUID from sqlspec.utils.serializers import to_json @@ -25,7 +25,7 @@ ParamTypeMatcher: TypeAlias = type | tuple[type, ...] | Callable[[object], bool] -_JSON_VALUE_TYPES = (dict, list, str, int, float, bool) +_JSON_VALUE_TYPES: Final[tuple[type, ...]] = (dict, list, str, int, float, bool) def _is_json_value(value: object) -> bool: @@ -39,7 +39,7 @@ def _is_json_value(value: object) -> bool: return True -_TYPE_REGISTRY: dict[str, ParamTypeMatcher] = { +_TYPE_REGISTRY: Final[dict[str, ParamTypeMatcher]] = { "str": str, "int": int, "float": float, diff --git a/sqlspec/core/parameters/_processor.py b/sqlspec/core/parameters/_processor.py index e83683fdc..36314a842 100644 --- a/sqlspec/core/parameters/_processor.py +++ b/sqlspec/core/parameters/_processor.py @@ -2,7 +2,7 @@ from collections import OrderedDict from collections.abc import Callable, Mapping, Sequence -from typing import Any, cast +from typing import Any, Final, cast from mypy_extensions import mypyc_attr @@ -27,18 +27,17 @@ __all__ = ("ParameterProcessor", "structural_fingerprint", "value_fingerprint") -# Threshold for sampling execute_many parameters instead of full iteration -_EXECUTE_MANY_SAMPLE_THRESHOLD = 10 -# Number of records to sample for type signatures -_EXECUTE_MANY_SAMPLE_SIZE = 3 -_OCCURRENCE_BASED_POSITIONAL_STYLES = frozenset({ +TypeCoercionFallback = tuple[type, Callable[[Any], Any]] + +_EXECUTE_MANY_SAMPLE_THRESHOLD: Final[int] = 10 +_EXECUTE_MANY_SAMPLE_SIZE: Final[int] = 3 +_OCCURRENCE_BASED_POSITIONAL_STYLES: Final[frozenset[ParameterStyle]] = frozenset({ ParameterStyle.QMARK, ParameterStyle.POSITIONAL_COLON, ParameterStyle.POSITIONAL_PYFORMAT, }) -TypeCoercionFallback = tuple[type, Callable[[Any], Any]] -_TYPE_COERCION_DISPATCHERS: "dict[tuple[TypeCoercionFallback, ...], TypeDispatcher[Callable[[Any], Any]]]" = {} +_TYPE_COERCION_DISPATCHERS: Final[dict[tuple[TypeCoercionFallback, ...], TypeDispatcher[Callable[[Any], Any]]]] = {} def structural_fingerprint(parameters: "ParameterPayload", is_many: bool = False) -> Any: @@ -1146,43 +1145,25 @@ def _coerce_parameters_payload( if updated_many is None: return seq_params return updated_many - - updated_seq: list[Any] | None = None - for idx, item in enumerate(seq_params): - coerced_item = _coerce_parameter_value(item, type_coercion_map, fallback_items) - if updated_seq is None: - if coerced_item is item: - continue - updated_seq = seq_params[:idx] - updated_seq.append(coerced_item) - if updated_seq is None: - return seq_params - return updated_seq + return _coerce_sequence_preserving_identity(seq_params, type_coercion_map, fallback_items) if param_type is tuple: tuple_params = cast("tuple[Any, ...]", parameters) if is_many: return [_coerce_parameter_set(param_set, type_coercion_map, fallback_items) for param_set in tuple_params] - return [_coerce_parameter_value(item, type_coercion_map, fallback_items) for item in tuple_params] + coerced_tuple = _coerce_sequence_preserving_identity(tuple_params, type_coercion_map, fallback_items) + return list(coerced_tuple) if param_type is dict: dict_params = cast("dict[Any, Any]", parameters) - updated_mapping: dict[Any, Any] | None = None - for key, val in dict_params.items(): - coerced_value = _coerce_parameter_value(val, type_coercion_map, fallback_items) - if updated_mapping is None: - if coerced_value is val: - continue - updated_mapping = dict(dict_params) - updated_mapping[key] = coerced_value - if updated_mapping is None: - return dict_params - return updated_mapping + return _coerce_mapping_preserving_identity(dict_params, type_coercion_map, fallback_items) # Fallback to ABC checks for custom types if is_many and isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): return [_coerce_parameter_set(param_set, type_coercion_map, fallback_items) for param_set in parameters] if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): - return [_coerce_parameter_value(item, type_coercion_map, fallback_items) for item in parameters] + coerced_sequence = _coerce_sequence_preserving_identity(parameters, type_coercion_map, fallback_items) + return list(coerced_sequence) if isinstance(parameters, Mapping): - return {key: _coerce_parameter_value(val, type_coercion_map, fallback_items) for key, val in parameters.items()} + coerced_mapping = _coerce_mapping_preserving_identity(parameters, type_coercion_map, fallback_items) + return dict(coerced_mapping) return _coerce_parameter_value(parameters, type_coercion_map, fallback_items) diff --git a/sqlspec/core/parameters/_registry.py b/sqlspec/core/parameters/_registry.py index f190cbb27..fd6189076 100644 --- a/sqlspec/core/parameters/_registry.py +++ b/sqlspec/core/parameters/_registry.py @@ -1,13 +1,15 @@ """Driver parameter profile registry and StatementConfig factory.""" -from collections.abc import Callable, Mapping -from typing import TYPE_CHECKING, Any, Literal, cast +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Final, Literal, cast import sqlspec.exceptions from sqlspec.core.parameters._types import DriverParameterProfile, ParameterStyleConfig from sqlspec.utils.serializers import from_json, to_json if TYPE_CHECKING: + from collections.abc import Mapping + from sqlspec.core.statement import StatementConfig __all__ = ( @@ -17,10 +19,10 @@ "register_driver_profile", ) -_DEFAULT_JSON_SERIALIZER: Callable[[Any], str] = to_json -_DEFAULT_JSON_DESERIALIZER: Callable[[str], Any] = from_json +_DEFAULT_JSON_SERIALIZER: Final[Callable[[Any], str]] = to_json +_DEFAULT_JSON_DESERIALIZER: Final[Callable[[str], Any]] = from_json -DRIVER_PARAMETER_PROFILES: "dict[str, DriverParameterProfile]" = {} +DRIVER_PARAMETER_PROFILES: Final[dict[str, DriverParameterProfile]] = {} def get_driver_profile(adapter_key: str) -> "DriverParameterProfile": diff --git a/sqlspec/core/parameters/_transformers.py b/sqlspec/core/parameters/_transformers.py index dd36748f2..ffc4f6f4c 100644 --- a/sqlspec/core/parameters/_transformers.py +++ b/sqlspec/core/parameters/_transformers.py @@ -2,7 +2,7 @@ import bisect from collections.abc import Callable, Mapping, Sequence -from typing import Any, cast +from typing import Any, Final, cast from mypy_extensions import mypyc_attr from sqlglot import exp as _exp @@ -32,7 +32,7 @@ ) -_MISSING_PARAMETER = object() +_MISSING_PARAMETER: Final = object() @mypyc_attr(allow_interpreted_subclasses=False) @@ -81,16 +81,16 @@ def __init__(self, null_positions: "set[int]", sorted_null_positions: "list[int] self._qmark_position = 0 def __call__(self, node: Any) -> Any: - if isinstance(node, _exp.Placeholder) and node.this is None: - current_position = self._qmark_position - self._qmark_position += 1 - if current_position in self._null_positions: - return _exp.Null() - return node + if isinstance(node, _exp.Placeholder): + placeholder_value = node.this + if placeholder_value is None: + current_position = self._qmark_position + self._qmark_position += 1 + if current_position in self._null_positions: + return _exp.Null() + return node - if isinstance(node, _exp.Placeholder) and node.this is not None: - placeholder_text = str(node.this) - normalized_text = placeholder_text.lstrip("$") + normalized_text = str(placeholder_value).lstrip("$") if normalized_text.isdigit(): param_index = int(normalized_text) - 1 if param_index in self._null_positions: @@ -119,12 +119,14 @@ def __call__(self, node: Any) -> Any: @mypyc_attr(allow_interpreted_subclasses=False) class _PlaceholderLiteralTransformer: - __slots__ = ("_json_serializer", "_parameters", "_placeholder_index") + __slots__ = ("_is_mapping", "_is_sequence", "_json_serializer", "_parameters", "_placeholder_index") def __init__(self, parameters: "ParameterPayload", json_serializer: "Callable[[Any], str]") -> None: self._parameters = parameters self._json_serializer = json_serializer self._placeholder_index = 0 + self._is_mapping = isinstance(parameters, Mapping) + self._is_sequence = isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, bytearray)) def _resolve_mapping_value(self, param_name: str, payload: "ParameterMapping") -> object: candidate_names = (param_name, f"@{param_name}", f":{param_name}", f"${param_name}", f"param_{param_name}") @@ -137,39 +139,38 @@ def _resolve_mapping_value(self, param_name: str, payload: "ParameterMapping") - return _MISSING_PARAMETER def __call__(self, node: Any) -> Any: - if ( - isinstance(node, _exp.Placeholder) - and isinstance(self._parameters, Sequence) - and not isinstance(self._parameters, (str, bytes, bytearray)) - ): + parameters = self._parameters + if isinstance(node, _exp.Placeholder) and self._is_sequence: + sequence_parameters = cast("Sequence[Any]", parameters) current_index = self._placeholder_index self._placeholder_index += 1 - if current_index < len(self._parameters): - literal_value = get_value_attribute(self._parameters[current_index]) + if current_index < len(sequence_parameters): + literal_value = get_value_attribute(sequence_parameters[current_index]) return _create_literal_expression(literal_value, self._json_serializer) return node if isinstance(node, _exp.Parameter): param_name = str(node.this) if node.this is not None else "" - if isinstance(self._parameters, Mapping): - resolved_value = self._resolve_mapping_value(param_name, self._parameters) + if self._is_mapping: + resolved_value = self._resolve_mapping_value(param_name, cast("ParameterMapping", parameters)) if resolved_value is not _MISSING_PARAMETER: return _create_literal_expression(resolved_value, self._json_serializer) return node - if isinstance(self._parameters, Sequence) and not isinstance(self._parameters, (str, bytes, bytearray)): + if self._is_sequence: + sequence_parameters = cast("Sequence[Any]", parameters) name = param_name try: if name.startswith("param_"): index_value = int(name[6:]) - if 0 <= index_value < len(self._parameters): - literal_value = get_value_attribute(self._parameters[index_value]) + if 0 <= index_value < len(sequence_parameters): + literal_value = get_value_attribute(sequence_parameters[index_value]) return _create_literal_expression(literal_value, self._json_serializer) if name.isdigit(): index_value = int(name) - if 0 <= index_value < len(self._parameters): - literal_value = get_value_attribute(self._parameters[index_value]) + if 0 <= index_value < len(sequence_parameters): + literal_value = get_value_attribute(sequence_parameters[index_value]) return _create_literal_expression(literal_value, self._json_serializer) except (ValueError, AttributeError): return node @@ -192,6 +193,20 @@ def build_literal_inlining_transform( return _LiteralInliningTransform(json_serializer) +def _as_concrete_payload(parameters: "ParameterPayload") -> "ConvertedParameters": + if parameters is None: + return None + if isinstance(parameters, dict): + return parameters + if isinstance(parameters, (list, tuple)): + return parameters + if isinstance(parameters, Mapping): + return dict(parameters) + if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): + return list(parameters) + return None + + def replace_null_parameters_with_literals( expression: Any, parameters: "ParameterPayload", @@ -218,15 +233,12 @@ def replace_null_parameters_with_literals( if isinstance(parameters, dict): return expression, parameters if isinstance(parameters, (list, tuple)): - return expression, list(parameters) if isinstance(parameters, list) else tuple(parameters) + return expression, _as_concrete_payload(parameters) return expression, None if is_many or looks_like_execute_many(parameters): - # For execute_many, convert to concrete type - if isinstance(parameters, dict): - return expression, parameters - if isinstance(parameters, (list, tuple)): - return expression, list(parameters) if isinstance(parameters, list) else tuple(parameters) + if isinstance(parameters, (dict, list, tuple)): + return expression, _as_concrete_payload(parameters) return expression, None if parameter_profile is None: @@ -236,16 +248,7 @@ def replace_null_parameters_with_literals( null_positions = collect_null_parameter_ordinals(parameters, parameter_profile) if not null_positions: - # Convert to concrete type for return - if isinstance(parameters, dict): - return expression, parameters - if isinstance(parameters, (list, tuple)): - return expression, list(parameters) if isinstance(parameters, list) else tuple(parameters) - if isinstance(parameters, Mapping): - return expression, dict(parameters) - if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)): - return expression, list(parameters) - return expression, None + return expression, _as_concrete_payload(parameters) null_names: set[str] = set() positional_null_positions: set[int] = set() diff --git a/sqlspec/core/parameters/_types.py b/sqlspec/core/parameters/_types.py index 9539bfcb3..888930ddc 100644 --- a/sqlspec/core/parameters/_types.py +++ b/sqlspec/core/parameters/_types.py @@ -5,7 +5,7 @@ from decimal import Decimal from enum import Enum from types import MappingProxyType -from typing import Any, Literal, TypeAlias +from typing import Any, Final, Literal, TypeAlias from mypy_extensions import mypyc_attr @@ -74,9 +74,9 @@ This is narrower than :data:`ConvertedParameters` and excludes ``list``, ``tuple``, and ``None``. """ -TYPED_PARAMETER_SLOTS = ("_hash", "original_type", "semantic_name", "value") -PARAMETER_INFO_SLOTS = ("name", "ordinal", "placeholder_text", "position", "style") -PARAMETER_STYLE_CONFIG_SLOTS = ( +TYPED_PARAMETER_SLOTS: Final[tuple[str, ...]] = ("_hash", "original_type", "semantic_name", "value") +PARAMETER_INFO_SLOTS: Final[tuple[str, ...]] = ("name", "ordinal", "placeholder_text", "position", "style") +PARAMETER_STYLE_CONFIG_SLOTS: Final[tuple[str, ...]] = ( "_hash_cache", "allow_mixed_parameter_styles", "ast_transformer", @@ -94,7 +94,7 @@ "supported_parameter_styles", "type_coercion_map", ) -DRIVER_PARAMETER_PROFILE_SLOTS = ( +DRIVER_PARAMETER_PROFILE_SLOTS: Final[tuple[str, ...]] = ( "allow_mixed_parameter_styles", "custom_type_coercions", "default_ast_transformer", @@ -114,8 +114,14 @@ "supported_execution_styles", "supported_styles", ) -PARAMETER_PROFILE_SLOTS = ("_parameters", "_placeholder_counts", "named_parameters", "reused_ordinals", "styles") -PARAMETER_PROCESSING_RESULT_SLOTS = ( +PARAMETER_PROFILE_SLOTS: Final[tuple[str, ...]] = ( + "_parameters", + "_placeholder_counts", + "named_parameters", + "reused_ordinals", + "styles", +) +PARAMETER_PROCESSING_RESULT_SLOTS: Final[tuple[str, ...]] = ( "applied_wrap_types", "input_named_parameters", "parameter_profile", @@ -142,20 +148,20 @@ class ParameterStyle(str, Enum): POSITIONAL_PYFORMAT = "pyformat_positional" -_NAMED_STYLES: frozenset["ParameterStyle"] = frozenset({ +_NAMED_STYLES: Final[frozenset["ParameterStyle"]] = frozenset({ ParameterStyle.NAMED_COLON, ParameterStyle.NAMED_AT, ParameterStyle.NAMED_DOLLAR, ParameterStyle.NAMED_PYFORMAT, }) -_POSITIONAL_STYLES: frozenset["ParameterStyle"] = frozenset({ +_POSITIONAL_STYLES: Final[frozenset["ParameterStyle"]] = frozenset({ ParameterStyle.QMARK, ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.POSITIONAL_PYFORMAT, }) -_NAMED_STYLE_VALUES: frozenset[str] = frozenset(style.value for style in _NAMED_STYLES) -_POSITIONAL_STYLE_VALUES: frozenset[str] = frozenset(style.value for style in _POSITIONAL_STYLES) +_NAMED_STYLE_VALUES: Final[frozenset[str]] = frozenset(style.value for style in _NAMED_STYLES) +_POSITIONAL_STYLE_VALUES: Final[frozenset[str]] = frozenset(style.value for style in _POSITIONAL_STYLES) @mypyc_attr(allow_interpreted_subclasses=False) @@ -491,10 +497,8 @@ def __init__(self, parameters: "Sequence[ParameterInfo] | None" = None) -> None: param_tuple: tuple[ParameterInfo, ...] = tuple(parameters) if parameters else () self._parameters = param_tuple - # Optimize styles computation: skip sorted() for single-style case (common) if param_tuple: unique_styles = {param.style.value for param in param_tuple} - # Skip sort for single style (common case) - O(1) vs O(n log n) if len(unique_styles) == 1: self.styles = (next(iter(unique_styles)),) else: diff --git a/sqlspec/core/parameters/_validator.py b/sqlspec/core/parameters/_validator.py index dc47ed1ac..657b13f4c 100644 --- a/sqlspec/core/parameters/_validator.py +++ b/sqlspec/core/parameters/_validator.py @@ -11,11 +11,9 @@ __all__ = ("PARAMETER_REGEX", "ParameterValidator") -# Pre-computed frozenset for fast parameter character detection -# Using set intersection is faster than any(c in sql for c in ...) -_PARAM_CHARS = frozenset("?%:@$") +_PARAM_CHARS: Final[frozenset[str]] = frozenset("?%:@$") -PARAMETER_REGEX = re.compile( +PARAMETER_REGEX: Final[re.Pattern[str]] = re.compile( r""" (?P"(?:[^"\\]|\\.)*") | (?P'(?:[^'\\]|\\.)*') | @@ -82,7 +80,8 @@ def cache_stats(self) -> "dict[str, int]": "max_size": self._cache_max_size, } - def _extract_parameter_style(self, match: re.Match[str]) -> "tuple[ParameterStyle | None, str | None]": + @staticmethod + def _extract_parameter_style(match: re.Match[str]) -> "tuple[ParameterStyle | None, str | None]": """Map a regex match to a placeholder style and optional name.""" if match.group("qmark"): return ParameterStyle.QMARK, None @@ -107,7 +106,6 @@ def extract_parameters(self, sql: str) -> "list[ParameterInfo]": if self._cache_max_size <= 0: return self._extract_parameters_uncached(sql) - # Use blake2b hash of SQL string as cache key for memory efficiency cache_key = hashlib.blake2b(sql.encode(), digest_size=8).hexdigest() cached_result = self._parameter_cache.get(cache_key) if cached_result is not None: @@ -116,26 +114,7 @@ def extract_parameters(self, sql: str) -> "list[ParameterInfo]": return cached_result self._cache_misses += 1 - # Fast check using frozenset intersection (faster than any() with generator) - if not _PARAM_CHARS.intersection(sql): - if len(self._parameter_cache) >= self._cache_max_size: - self._parameter_cache.popitem(last=False) - self._parameter_cache[cache_key] = [] - return [] - - parameters: list[ParameterInfo] = [] - ordinal = 0 - - for match in PARAMETER_REGEX.finditer(sql): - if any(match.group(group) for group in _SKIP_GROUPS): - continue - style, name = self._extract_parameter_style(match) - if style is None: - continue - placeholder_text = match.group(0) - parameters.append(ParameterInfo(name, style, match.start(), ordinal, placeholder_text)) - ordinal += 1 - + parameters = self._extract_parameters_uncached(sql) if len(self._parameter_cache) >= self._cache_max_size: self._parameter_cache.popitem(last=False) self._parameter_cache[cache_key] = parameters @@ -145,12 +124,11 @@ def _extract_parameters_uncached(self, sql: str) -> "list[ParameterInfo]": parameters: list[ParameterInfo] = [] ordinal = 0 - # Fast check using frozenset intersection (faster than any() with generator) if not _PARAM_CHARS.intersection(sql): return [] for match in PARAMETER_REGEX.finditer(sql): - if any(match.group(group) for group in _SKIP_GROUPS): + if any(match.group(*_SKIP_GROUPS)): continue style, name = self._extract_parameter_style(match) if style is None: diff --git a/sqlspec/core/pipeline.py b/sqlspec/core/pipeline.py index b365057b8..833914f46 100644 --- a/sqlspec/core/pipeline.py +++ b/sqlspec/core/pipeline.py @@ -18,6 +18,7 @@ __all__ = ( "StatementPipelineRegistry", "compile_with_pipeline", + "configure_statement_pipeline_cache", "get_statement_pipeline_metrics", "reset_statement_pipeline_cache", ) @@ -34,101 +35,45 @@ def _is_truthy(value: "str | None") -> bool: _RECORD_PIPELINE_METRICS: Final[bool] = _is_truthy(os.environ.get(DEBUG_ENV_FLAG)) +_METRIC_KEYS: Final[tuple[str, ...]] = ( + "hits", + "misses", + "size", + "max_size", + "parse_hits", + "parse_misses", + "parse_size", + "parse_max_size", + "parameter_hits", + "parameter_misses", + "parameter_size", + "parameter_max_size", + "validator_hits", + "validator_misses", + "validator_size", + "validator_max_size", +) + @mypyc_attr(allow_interpreted_subclasses=False) class _PipelineMetrics: - __slots__ = ( - "hits", - "max_size", - "misses", - "parameter_hits", - "parameter_max_size", - "parameter_misses", - "parameter_size", - "parse_hits", - "parse_max_size", - "parse_misses", - "parse_size", - "size", - "validator_hits", - "validator_max_size", - "validator_misses", - "validator_size", - ) + __slots__ = ("_values",) def __init__(self) -> None: - self.hits = 0 - self.misses = 0 - self.size = 0 - self.max_size = 0 - self.parse_hits = 0 - self.parse_misses = 0 - self.parse_size = 0 - self.parse_max_size = 0 - self.parameter_hits = 0 - self.parameter_misses = 0 - self.parameter_size = 0 - self.parameter_max_size = 0 - self.validator_hits = 0 - self.validator_misses = 0 - self.validator_size = 0 - self.validator_max_size = 0 + self._values = dict.fromkeys(_METRIC_KEYS, 0) def update(self, stats: "dict[str, int]") -> None: - self.hits = stats.get("hits", 0) - self.misses = stats.get("misses", 0) - self.size = stats.get("size", 0) - self.max_size = stats.get("max_size", 0) - self.parse_hits = stats.get("parse_hits", 0) - self.parse_misses = stats.get("parse_misses", 0) - self.parse_size = stats.get("parse_size", 0) - self.parse_max_size = stats.get("parse_max_size", 0) - self.parameter_hits = stats.get("parameter_hits", 0) - self.parameter_misses = stats.get("parameter_misses", 0) - self.parameter_size = stats.get("parameter_size", 0) - self.parameter_max_size = stats.get("parameter_max_size", 0) - self.validator_hits = stats.get("validator_hits", 0) - self.validator_misses = stats.get("validator_misses", 0) - self.validator_size = stats.get("validator_size", 0) - self.validator_max_size = stats.get("validator_max_size", 0) + values = self._values + for key in _METRIC_KEYS: + values[key] = stats.get(key, 0) def snapshot(self) -> "dict[str, int]": - return { - "hits": self.hits, - "misses": self.misses, - "size": self.size, - "max_size": self.max_size, - "parse_hits": self.parse_hits, - "parse_misses": self.parse_misses, - "parse_size": self.parse_size, - "parse_max_size": self.parse_max_size, - "parameter_hits": self.parameter_hits, - "parameter_misses": self.parameter_misses, - "parameter_size": self.parameter_size, - "parameter_max_size": self.parameter_max_size, - "validator_hits": self.validator_hits, - "validator_misses": self.validator_misses, - "validator_size": self.validator_size, - "validator_max_size": self.validator_max_size, - } + return self._values.copy() def reset(self) -> None: - self.hits = 0 - self.misses = 0 - self.size = 0 - self.max_size = 0 - self.parse_hits = 0 - self.parse_misses = 0 - self.parse_size = 0 - self.parse_max_size = 0 - self.parameter_hits = 0 - self.parameter_misses = 0 - self.parameter_size = 0 - self.parameter_max_size = 0 - self.validator_hits = 0 - self.validator_misses = 0 - self.validator_size = 0 - self.validator_max_size = 0 + values = self._values + for key in _METRIC_KEYS: + values[key] = 0 @mypyc_attr(allow_interpreted_subclasses=False) @@ -252,34 +197,20 @@ def metrics(self) -> "list[dict[str, Any]]": "dialect": pipeline.dialect, "parameter_style": pipeline.parameter_style, } - entry["hits"] = metrics["hits"] - entry["misses"] = metrics["misses"] - entry["size"] = metrics["size"] - entry["max_size"] = metrics["max_size"] - entry["parse_hits"] = metrics.get("parse_hits", 0) - entry["parse_misses"] = metrics.get("parse_misses", 0) - entry["parse_size"] = metrics.get("parse_size", 0) - entry["parse_max_size"] = metrics.get("parse_max_size", 0) - entry["parameter_hits"] = metrics.get("parameter_hits", 0) - entry["parameter_misses"] = metrics.get("parameter_misses", 0) - entry["parameter_size"] = metrics.get("parameter_size", 0) - entry["parameter_max_size"] = metrics.get("parameter_max_size", 0) - entry["validator_hits"] = metrics.get("validator_hits", 0) - entry["validator_misses"] = metrics.get("validator_misses", 0) - entry["validator_size"] = metrics.get("validator_size", 0) - entry["validator_max_size"] = metrics.get("validator_max_size", 0) + entry.update(metrics) snapshots.append(entry) return snapshots - def _fingerprint_config(self, config: "Any") -> str: - # Optimization: Use cached fingerprint if available - # Configs are effectively immutable after creation, so caching is safe - try: - cached = config._fingerprint_cache # pyright: ignore[reportPrivateUsage] - if isinstance(cached, str): - return cached - except AttributeError: - pass + @staticmethod + def _fingerprint_config(config: "Any") -> str: + is_frozen = bool(getattr(config, "_is_frozen", False)) + if is_frozen: + try: + cached = config._fingerprint_cache # pyright: ignore[reportPrivateUsage] + if isinstance(cached, str): + return cached + except AttributeError: + pass config_hash = hash(config) param_config = config.parameter_config @@ -291,9 +222,9 @@ def _fingerprint_config(self, config: "Any") -> str: fingerprint = hashlib.blake2b(repr((config_hash, supplement)).encode(), digest_size=8).hexdigest() full_fingerprint = f"pipeline::{fingerprint}" - # Cache the fingerprint for future calls - configs are immutable in practice - with contextlib.suppress(AttributeError): - config._fingerprint_cache = full_fingerprint # pyright: ignore[reportPrivateUsage] + if is_frozen: + with contextlib.suppress(AttributeError): + config._fingerprint_cache = full_fingerprint # pyright: ignore[reportPrivateUsage] return full_fingerprint diff --git a/sqlspec/core/query_modifiers.py b/sqlspec/core/query_modifiers.py index dda6c28cd..3547141ac 100644 --- a/sqlspec/core/query_modifiers.py +++ b/sqlspec/core/query_modifiers.py @@ -12,7 +12,7 @@ """ from collections.abc import Callable -from typing import Any +from typing import Any, Final from sqlglot import exp @@ -50,9 +50,9 @@ # Type alias for condition factory functions ConditionFactory = Callable[[exp.Expr, exp.Placeholder], exp.Expr] -_TABLE_QUALIFIED_PARTS = 2 -_DATABASE_QUALIFIED_PARTS = 3 -_CATALOG_QUALIFIED_PARTS = 4 +_TABLE_QUALIFIED_PARTS: Final[int] = 2 +_DATABASE_QUALIFIED_PARTS: Final[int] = 3 +_CATALOG_QUALIFIED_PARTS: Final[int] = 4 # ============================================================================= diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index eec4bad5a..86a577c96 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -11,7 +11,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterable, Iterator, Sequence -from typing import TYPE_CHECKING, Any, cast, overload +from typing import TYPE_CHECKING, Any, Final, cast, overload from mypy_extensions import mypyc_attr from typing_extensions import TypeVar @@ -48,10 +48,10 @@ __all__ = ("ArrowResult", "DMLResult", "EmptyResult", "SQLResult", "StackResult", "StatementResult") T = TypeVar("T") -_EMPTY_RESULT_STATEMENT = SQL("-- empty stack result --") -_EMPTY_RESULT_DATA: "tuple[()]" = () -_DEFAULT_DML_METADATA: dict[str, Any] = {} -_TWO_COLUMN_THRESHOLD = 2 +_EMPTY_RESULT_STATEMENT: Final = SQL("-- empty stack result --") +_EMPTY_RESULT_DATA: Final[tuple[Any, ...]] = () +_DEFAULT_DML_METADATA: Final[dict[str, Any]] = {} +_TWO_COLUMN_THRESHOLD: Final[int] = 2 @mypyc_attr(allow_interpreted_subclasses=False) @@ -828,6 +828,8 @@ def num_rows(self) -> int: def num_columns(self) -> int: """Get the number of columns in the Arrow table. + External/extension API: not called internally. + Returns: Number of columns. """ @@ -898,10 +900,10 @@ def __len__(self) -> int: def __iter__(self) -> "Iterator[dict[str, Any]]": """Iterate over rows as dictionaries. - Yields: - Dictionary for each row. + Returns: + Iterator of row dictionaries. """ - yield from arrow_table_to_pylist(self._as_table()) + return iter(arrow_table_to_pylist(self._as_table())) class EmptyResult(StatementResult): @@ -997,6 +999,7 @@ def __init__( self.rows_affected = rows_affected else: try: + # Direct access on this compiled union currently segfaults mypyc during full-graph builds. result_rows = object.__getattribute__(self.result, "rows_affected") except AttributeError: self.rows_affected = 0 @@ -1027,7 +1030,10 @@ def is_sql_result(self) -> bool: return isinstance(self.result, StatementResult) and not isinstance(self.result, ArrowResult) def is_arrow_result(self) -> bool: - """Return True when the underlying result is an ArrowResult.""" + """Return True when the underlying result is an ArrowResult. + + External/extension API: not called internally. + """ return isinstance(self.result, ArrowResult) @@ -1057,7 +1063,10 @@ def from_sql_result(cls, result: "SQLResult") -> "StackResult": @classmethod def from_arrow_result(cls, result: "ArrowResult") -> "StackResult": - """Create a stack result from an ArrowResult instance.""" + """Create a stack result from an ArrowResult instance. + + External/extension API: not called internally. + """ metadata = result.metadata or None return cls(result=result, rows_affected=result.rows_affected, metadata=metadata) diff --git a/sqlspec/core/splitter.py b/sqlspec/core/splitter.py index bae926d44..40e93fa1a 100644 --- a/sqlspec/core/splitter.py +++ b/sqlspec/core/splitter.py @@ -17,7 +17,7 @@ import re import threading from abc import ABC, abstractmethod -from collections.abc import Callable, Generator +from collections.abc import Callable from enum import Enum from re import Pattern from typing import Any, Final, TypeAlias, cast, final @@ -42,6 +42,7 @@ _TOKENIZE_DEBUG_SAMPLE_LIMIT: Final[int] = 3 _TOKENIZE_SNIPPET_LENGTH: Final[int] = 20 +_DOLLAR_QUOTE_START_PATTERN: Final[Pattern[str]] = re.compile(r"\$([a-zA-Z_][a-zA-Z0-9_]*)?\$") DEFAULT_PATTERN_CACHE_SIZE: Final = 1000 DEFAULT_RESULT_CACHE_SIZE: Final = 5000 @@ -107,6 +108,15 @@ class TokenType(Enum): OTHER = "OTHER" +_COMMENT_TOKEN_TYPES: Final[frozenset[TokenType]] = frozenset({TokenType.COMMENT_LINE, TokenType.COMMENT_BLOCK}) +_IGNORABLE_TOKEN_TYPES: Final[frozenset[TokenType]] = frozenset({ + TokenType.WHITESPACE, + TokenType.COMMENT_LINE, + TokenType.COMMENT_BLOCK, +}) +_SLASH_PREFIX_TOKEN_TYPES: Final[frozenset[TokenType]] = frozenset({TokenType.WHITESPACE, TokenType.COMMENT_LINE}) + + @mypyc_attr(allow_interpreted_subclasses=False) class Token: """SQL token with metadata.""" @@ -127,6 +137,7 @@ def __repr__(self) -> str: TokenHandler: TypeAlias = Callable[[str, int, int, int], Token | None] TokenPattern: TypeAlias = str | TokenHandler CompiledTokenPattern: TypeAlias = Pattern[str] | TokenHandler +SpecialTerminatorHandler: TypeAlias = Callable[[list[Token], int], bool] @mypyc_attr(allow_interpreted_subclasses=False) @@ -248,38 +259,78 @@ def should_delay_semicolon_termination(self, tokens: "list[Token]", current_pos: return False -class OracleDialectConfig(DialectConfig): - """Configuration for Oracle PL/SQL dialect.""" +class _EagerDialectConfig(DialectConfig): + """Private eager base for built-in dialect configurations.""" + + __slots__ = () + + _DIALECT_NAME = "" + _BLOCK_STARTERS: frozenset[str] = frozenset() + _BLOCK_ENDERS: frozenset[str] = frozenset({"END"}) + _STATEMENT_TERMINATORS: frozenset[str] = frozenset({";"}) + _BATCH_SEPARATORS: frozenset[str] = frozenset() + _SPECIAL_TERMINATORS: tuple[tuple[str, SpecialTerminatorHandler], ...] = () + _MAX_NESTING_DEPTH = 256 + + def __init__(self) -> None: + """Initialize eager dialect configuration.""" + self._name = self._DIALECT_NAME + self._block_starters = set(self._BLOCK_STARTERS) + self._block_enders = set(self._BLOCK_ENDERS) + self._statement_terminators = set(self._STATEMENT_TERMINATORS) + self._batch_separators = set(self._BATCH_SEPARATORS) + self._special_terminators = self._create_special_terminators() + self._max_nesting_depth = self._MAX_NESTING_DEPTH @property def name(self) -> str: - if self._name is None: - self._name = "oracle" - return self._name + """Name of the dialect.""" + return cast("str", self._name) @property def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "DECLARE", "CASE"} - return self._block_starters + """Keywords that start a block.""" + return cast("set[str]", self._block_starters) @property def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders + """Keywords that end a block.""" + return cast("set[str]", self._block_enders) @property def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + """Characters that terminate statements.""" + return cast("set[str]", self._statement_terminators) @property - def special_terminators(self) -> "dict[str, Callable[[list[Token], int], bool]]": - if self._special_terminators is None: - self._special_terminators = {"/": self._handle_slash_terminator} - return self._special_terminators + def batch_separators(self) -> "set[str]": + """Keywords that separate batches.""" + return cast("set[str]", self._batch_separators) + + @property + def special_terminators(self) -> "dict[str, SpecialTerminatorHandler]": + """Special terminators that need custom handling.""" + return cast("dict[str, SpecialTerminatorHandler]", self._special_terminators) + + @property + def max_nesting_depth(self) -> int: + """Maximum allowed nesting depth for blocks.""" + return cast("int", self._max_nesting_depth) + + def _create_special_terminators(self) -> "dict[str, SpecialTerminatorHandler]": + """Create per-instance special terminators.""" + return dict(self._SPECIAL_TERMINATORS) + + +class OracleDialectConfig(_EagerDialectConfig): + """Configuration for Oracle PL/SQL dialect.""" + + _DIALECT_NAME = "oracle" + _BLOCK_STARTERS = frozenset({"BEGIN", "DECLARE", "CASE"}) + + def _create_special_terminators(self) -> "dict[str, SpecialTerminatorHandler]": + """Create Oracle special terminator handlers.""" + return {"/": self._handle_slash_terminator} def should_delay_semicolon_termination(self, tokens: "list[Token]", current_pos: int) -> bool: """Check if semicolon termination should be delayed for Oracle slash terminators. @@ -325,7 +376,7 @@ def _has_upcoming_slash(self, tokens: "list[Token]", current_pos: int) -> bool: continue if token.type == TokenType.TERMINATOR and token.value == "/": return found_newline and self._handle_slash_terminator(tokens, pos) - if token.type in {TokenType.COMMENT_LINE, TokenType.COMMENT_BLOCK}: + if token.type in _COMMENT_TOKEN_TYPES: pos += 1 continue break @@ -385,73 +436,27 @@ def _handle_slash_terminator(tokens: "list[Token]", current_pos: int) -> bool: token = tokens[pos] if "\n" in token.value: break - if token.type not in {TokenType.WHITESPACE, TokenType.COMMENT_LINE}: + if token.type not in _SLASH_PREFIX_TOKEN_TYPES: return False pos -= 1 return True -class TSQLDialectConfig(DialectConfig): +class TSQLDialectConfig(_EagerDialectConfig): """Configuration for T-SQL (SQL Server) dialect.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "tsql" - return self._name + _DIALECT_NAME = "tsql" + _BLOCK_STARTERS = frozenset({"BEGIN", "TRY"}) + _BLOCK_ENDERS = frozenset({"END", "CATCH"}) + _BATCH_SEPARATORS = frozenset({"GO"}) - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "TRY"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END", "CATCH"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators - @property - def batch_separators(self) -> "set[str]": - if self._batch_separators is None: - self._batch_separators = {"GO"} - return self._batch_separators - - -class PostgreSQLDialectConfig(DialectConfig): +class PostgreSQLDialectConfig(_EagerDialectConfig): """Configuration for PostgreSQL dialect with dollar-quoted strings.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "postgresql" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"DECLARE", "CASE", "DO"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + _DIALECT_NAME = "postgresql" + _BLOCK_STARTERS = frozenset({"DECLARE", "CASE", "DO"}) def _get_dialect_specific_patterns(self) -> "list[tuple[TokenType, TokenPattern]]": """Get PostgreSQL-specific token patterns. @@ -477,7 +482,7 @@ def _handle_dollar_quoted_string(text: str, position: int, line: int, column: in Returns: Token representing the dollar-quoted string, or None if no match """ - start_match = re.match(r"\$([a-zA-Z_][a-zA-Z0-9_]*)?\$", text[position:]) + start_match = _DOLLAR_QUOTE_START_PATTERN.match(text, position) if not start_match: return None @@ -494,154 +499,47 @@ def _handle_dollar_quoted_string(text: str, position: int, line: int, column: in @final -class GenericDialectConfig(DialectConfig): +class GenericDialectConfig(_EagerDialectConfig): """Generic SQL dialect configuration for standard SQL.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "generic" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "DECLARE", "CASE"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + _DIALECT_NAME = "generic" + _BLOCK_STARTERS = frozenset({"BEGIN", "DECLARE", "CASE"}) @final -class MySQLDialectConfig(DialectConfig): +class MySQLDialectConfig(_EagerDialectConfig): """Configuration for MySQL dialect.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "mysql" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "DECLARE", "CASE"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators - - @property - def special_terminators(self) -> "dict[str, Callable[[list[Token], int], bool]]": - if self._special_terminators is None: - self._special_terminators = {"\\g": _special_terminator_true, "\\G": _special_terminator_true} - return self._special_terminators + _DIALECT_NAME = "mysql" + _BLOCK_STARTERS = frozenset({"BEGIN", "DECLARE", "CASE"}) + _SPECIAL_TERMINATORS: tuple[tuple[str, SpecialTerminatorHandler], ...] = ( + ("\\g", _special_terminator_true), + ("\\G", _special_terminator_true), + ) @final -class SQLiteDialectConfig(DialectConfig): +class SQLiteDialectConfig(_EagerDialectConfig): """Configuration for SQLite dialect.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "sqlite" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "CASE"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + _DIALECT_NAME = "sqlite" + _BLOCK_STARTERS = frozenset({"BEGIN", "CASE"}) @final -class DuckDBDialectConfig(DialectConfig): +class DuckDBDialectConfig(_EagerDialectConfig): """Configuration for DuckDB dialect.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "duckdb" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "CASE"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + _DIALECT_NAME = "duckdb" + _BLOCK_STARTERS = frozenset({"BEGIN", "CASE"}) @final -class BigQueryDialectConfig(DialectConfig): +class BigQueryDialectConfig(_EagerDialectConfig): """Configuration for BigQuery dialect.""" - @property - def name(self) -> str: - if self._name is None: - self._name = "bigquery" - return self._name - - @property - def block_starters(self) -> "set[str]": - if self._block_starters is None: - self._block_starters = {"BEGIN", "CASE"} - return self._block_starters - - @property - def block_enders(self) -> "set[str]": - if self._block_enders is None: - self._block_enders = {"END"} - return self._block_enders - - @property - def statement_terminators(self) -> "set[str]": - if self._statement_terminators is None: - self._statement_terminators = {";"} - return self._statement_terminators + _DIALECT_NAME = "bigquery" + _BLOCK_STARTERS = frozenset({"BEGIN", "CASE"}) _DIALECT_CLASS_MAP: Final[dict[str, type[DialectConfig]]] = { @@ -664,8 +562,10 @@ def statement_terminators(self) -> "set[str]": _pattern_cache: LRUCache | None = None _result_cache: LRUCache | None = None _cache_lock = threading.Lock() +_splitter_cache_lock = threading.Lock() _unknown_dialect_warning_lock = threading.Lock() _warned_unknown_dialects: set[str] = set() +_splitter_cache: dict[tuple[str, bool], "StatementSplitter"] = {} def _get_pattern_cache() -> LRUCache: @@ -752,18 +652,19 @@ def _get_or_compile_patterns(self) -> "list[tuple[TokenType, CompiledTokenPatter self._pattern_cache.put(cache_key, compiled) return compiled - def _tokenize(self, sql: str) -> Generator[Token, None, None]: + def _tokenize(self, sql: str) -> list[Token]: """Tokenize SQL string into Token objects. Args: sql: The SQL string to tokenize - Yields: + Returns: Token objects representing the lexical elements """ pos = 0 line = 1 line_start = 0 + tokens: list[Token] = [] unmatched_count = 0 first_unmatched_pos: int | None = None first_unmatched_snippet: str | None = None @@ -782,7 +683,7 @@ def _tokenize(self, sql: str) -> Generator[Token, None, None]: last_newline = token.value.rfind("\n") line_start = pos + last_newline + 1 - yield token + tokens.append(token) pos += len(token.value) matched = True break @@ -798,7 +699,7 @@ def _tokenize(self, sql: str) -> Generator[Token, None, None]: last_newline = value.rfind("\n") line_start = pos + last_newline + 1 - yield Token(type=token_type, value=value, line=line, column=column, position=pos) + tokens.append(Token(type=token_type, value=value, line=line, column=column, position=pos)) pos = match.end() matched = True break @@ -821,6 +722,7 @@ def _tokenize(self, sql: str) -> Generator[Token, None, None]: first_unmatched_pos, first_unmatched_snippet, ) + return tokens def split(self, sql: str) -> "list[str]": """Split SQL script into individual statements. @@ -858,8 +760,17 @@ def _do_split(self, sql: str) -> "list[str]": current_statement_chars: list[str] = [] current_statement_fragment_count = 0 block_stack = [] - - all_tokens = list(self._tokenize(sql)) + dialect = self._dialect + block_starters = dialect.block_starters + block_enders = dialect.block_enders + statement_terminators = dialect.statement_terminators + special_terminators = dialect.special_terminators + batch_separators = dialect.batch_separators + max_nesting_depth = dialect.max_nesting_depth + is_real_block_ender = dialect.is_real_block_ender + should_delay_semicolon_termination = dialect.should_delay_semicolon_termination + + all_tokens = self._tokenize(sql) for token_idx, token in enumerate(all_tokens): current_statement_fragment_count += 1 @@ -870,39 +781,39 @@ def _do_split(self, sql: str) -> "list[str]": # Keep token-list mutation centralized for whitespace/comment and executable paths. current_statement_tokens.append(token) - if token.type in {TokenType.WHITESPACE, TokenType.COMMENT_LINE, TokenType.COMMENT_BLOCK}: + if token.type in _IGNORABLE_TOKEN_TYPES: continue token_upper = token.value.upper() if token.type == TokenType.KEYWORD: - if token_upper in self._dialect.block_starters: + if token_upper in block_starters: block_stack.append(token_upper) - if len(block_stack) > self._dialect.max_nesting_depth: - msg = f"Maximum nesting depth ({self._dialect.max_nesting_depth}) exceeded" + if len(block_stack) > max_nesting_depth: + msg = f"Maximum nesting depth ({max_nesting_depth}) exceeded" raise ValueError(msg) - elif token_upper in self._dialect.block_enders: - if block_stack and self._dialect.is_real_block_ender(all_tokens, token_idx): + elif token_upper in block_enders: + if block_stack and is_real_block_ender(all_tokens, token_idx): block_stack.pop() is_terminator = False if not block_stack: if token.type == TokenType.TERMINATOR: - if token.value in self._dialect.statement_terminators: - should_delay = self._dialect.should_delay_semicolon_termination(all_tokens, token_idx) + if token.value in statement_terminators: + should_delay = should_delay_semicolon_termination(all_tokens, token_idx) if not should_delay: is_terminator = True - elif token.value in self._dialect.special_terminators: - handler = self._dialect.special_terminators[token.value] + elif token.value in special_terminators: + handler = special_terminators[token.value] if handler(all_tokens, token_idx): is_terminator = True - elif token.type == TokenType.KEYWORD and token_upper in self._dialect.batch_separators: + elif token.type == TokenType.KEYWORD and token_upper in batch_separators: is_terminator = True if is_terminator: - if token.type == TokenType.KEYWORD and token_upper in self._dialect.batch_separators: + if token.type == TokenType.KEYWORD and token_upper in batch_separators: statement = "".join(item.value for item in current_statement_tokens[:-1]).strip() elif current_statement_writer is None: statement = "".join(current_statement_chars).strip() @@ -961,11 +872,7 @@ def _contains_executable_content(self, tokens: "list[Token]") -> bool: Returns: True if statement contains non-whitespace/non-comment content """ - for token in tokens: - if token.type not in {TokenType.WHITESPACE, TokenType.COMMENT_LINE, TokenType.COMMENT_BLOCK}: - return True - - return False + return any(token.type not in _IGNORABLE_TOKEN_TYPES for token in tokens) def split_sql_script(script: str, dialect: str | None = None, strip_trailing_terminator: bool = False) -> "list[str]": @@ -979,16 +886,21 @@ def split_sql_script(script: str, dialect: str | None = None, strip_trailing_ter Returns: List of individual SQL statements """ - if dialect is None: - dialect = "generic" + dialect_key = "generic" if dialect is None else dialect.lower() - config_class = _DIALECT_CLASS_MAP.get(dialect.lower()) + config_class = _DIALECT_CLASS_MAP.get(dialect_key) if config_class is None: _warn_unknown_dialect_once(dialect) config_class = GenericDialectConfig - config = config_class() - splitter = StatementSplitter(config, strip_trailing_semicolon=strip_trailing_terminator) + cache_key = (dialect_key, strip_trailing_terminator) + splitter = _splitter_cache.get(cache_key) + if splitter is None: + with _splitter_cache_lock: + splitter = _splitter_cache.get(cache_key) + if splitter is None: + splitter = StatementSplitter(config_class(), strip_trailing_semicolon=strip_trailing_terminator) + _splitter_cache[cache_key] = splitter return splitter.split(script) @@ -1001,6 +913,8 @@ def clear_splitter_caches() -> None: result_cache = _get_result_cache() pattern_cache.clear() result_cache.clear() + with _splitter_cache_lock: + _splitter_cache.clear() with _unknown_dialect_warning_lock: _warned_unknown_dialects.clear() diff --git a/sqlspec/core/sqlcommenter.py b/sqlspec/core/sqlcommenter.py index 0fa580f89..1b1db3fb4 100644 --- a/sqlspec/core/sqlcommenter.py +++ b/sqlspec/core/sqlcommenter.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Generator, Mapping from contextlib import contextmanager from contextvars import ContextVar -from typing import Any, ClassVar, TypedDict +from typing import Any, ClassVar, Final, TypedDict, final from urllib.parse import quote, unquote from sqlglot import exp @@ -39,7 +39,7 @@ class SQLCommenterAttributes(TypedDict, total=False): tracestate: str -_sqlcommenter_ctx: ContextVar[dict[str, str] | None] = ContextVar("_sqlcommenter_ctx", default=None) +_sqlcommenter_ctx: Final[ContextVar[dict[str, str] | None]] = ContextVar("_sqlcommenter_ctx", default=None) class SQLCommenterContext: @@ -183,6 +183,58 @@ def _build_traceparent(trace_id: str, span_id: str) -> str: return f"00-{trace_id}-{span_id}-01" +@final +class _NoOpSQLCommenterTransformer: + __slots__ = () + + def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: + return expression, params + + +@final +class _StaticSQLCommenterTransformer: + __slots__ = ("_comment_body",) + + def __init__(self, attrs: Mapping[str, str | None]) -> None: + self._comment_body = generate_comment(attrs) + + def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: + if self._comment_body: + expression.add_comments([self._comment_body]) + return expression, params + + +@final +class _DynamicSQLCommenterTransformer: + __slots__ = ("_enable_context", "_enable_traceparent", "_static_attrs") + + def __init__( + self, static_attrs: Mapping[str, str | None], *, enable_traceparent: bool, enable_context: bool + ) -> None: + self._static_attrs = dict(static_attrs) + self._enable_traceparent = enable_traceparent + self._enable_context = enable_context + + def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: + merged: dict[str, str | None] = {} + if self._enable_context: + ctx_attrs = SQLCommenterContext.get() + if ctx_attrs: + merged.update(ctx_attrs) + correlation_id = CorrelationContext.get() + if correlation_id and "correlation_id" not in merged: + merged["correlation_id"] = correlation_id + merged.update(self._static_attrs) + if self._enable_traceparent: + trace_id, span_id = get_trace_context() + if trace_id and span_id: + merged["traceparent"] = _build_traceparent(trace_id, span_id) + return append_comment(expression, merged), params + + +_NOOP_SQLCOMMENTER_TRANSFORMER: Final = _NoOpSQLCommenterTransformer() + + def create_sqlcommenter_statement_transformer( *, attributes: dict[str, str | None] | None = None, enable_traceparent: bool = False, enable_context: bool = False ) -> Callable[[exp.Expr, Any], tuple[exp.Expr, Any]]: @@ -206,42 +258,11 @@ def create_sqlcommenter_statement_transformer( is_dynamic = enable_traceparent or enable_context if not is_dynamic and not static_attrs: - - def _noop_transformer(expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: - return expression, params - - return _noop_transformer + return _NOOP_SQLCOMMENTER_TRANSFORMER if not is_dynamic: - # Pure static path — pre-generate the comment body once. - precomputed_body = generate_comment(static_attrs) - - def _static_transformer(expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: - if precomputed_body: - expression.add_comments([precomputed_body]) - return expression, params - - return _static_transformer - - def _dynamic_transformer(expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: - merged: dict[str, str | None] = {} - if enable_context: - ctx_attrs = SQLCommenterContext.get() - if ctx_attrs: - merged.update(ctx_attrs) - # Include correlation ID from CorrelationContext if set - correlation_id = CorrelationContext.get() - if correlation_id and "correlation_id" not in merged: - merged["correlation_id"] = correlation_id - # Static attrs override context attrs - merged.update(static_attrs) - if enable_traceparent: - trace_id, span_id = get_trace_context() - if trace_id and span_id: - merged["traceparent"] = _build_traceparent(trace_id, span_id) - comment_body = generate_comment(merged) - if comment_body: - expression.add_comments([comment_body]) - return expression, params + return _StaticSQLCommenterTransformer(static_attrs) - return _dynamic_transformer + return _DynamicSQLCommenterTransformer( + static_attrs, enable_traceparent=enable_traceparent, enable_context=enable_context + ) diff --git a/sqlspec/core/stack.py b/sqlspec/core/stack.py index 928136d22..99fe62b84 100644 --- a/sqlspec/core/stack.py +++ b/sqlspec/core/stack.py @@ -2,7 +2,7 @@ from collections.abc import Iterator, Mapping, Sequence from types import MappingProxyType -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final from mypy_extensions import mypyc_attr @@ -14,7 +14,7 @@ __all__ = ("StackOperation", "StatementStack") -ALLOWED_METHODS: "tuple[str, ...]" = ("execute", "execute_many", "execute_script", "execute_arrow") +ALLOWED_METHODS: Final[tuple[str, ...]] = ("execute", "execute_many", "execute_script", "execute_arrow") @mypyc_attr(allow_interpreted_subclasses=False) @@ -91,7 +91,7 @@ def push_execute_many( normalized_sets = tuple(parameter_sets) arguments = (normalized_sets, *filters) frozen_kwargs = _freeze_kwargs(kwargs, statement_config) - operation = StackOperation("execute_many", normalized_statement, tuple(arguments), frozen_kwargs) + operation = StackOperation("execute_many", normalized_statement, arguments, frozen_kwargs) return self._append(operation) def push_execute_script( diff --git a/sqlspec/core/statement.py b/sqlspec/core/statement.py index 356bdef9a..79e79097a 100644 --- a/sqlspec/core/statement.py +++ b/sqlspec/core/statement.py @@ -75,8 +75,16 @@ ) logger = get_logger("sqlspec.core.statement") -RETURNS_ROWS_OPERATIONS: Final = {"SELECT", "WITH", "VALUES", "TABLE", "SHOW", "DESCRIBE", "PRAGMA"} -MODIFYING_OPERATIONS: Final = {"INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT"} +RETURNS_ROWS_OPERATIONS: Final[frozenset[str]] = frozenset({ + "SELECT", + "WITH", + "VALUES", + "TABLE", + "SHOW", + "DESCRIBE", + "PRAGMA", +}) +MODIFYING_OPERATIONS: Final[frozenset[str]] = frozenset({"INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT"}) _ORDER_PARTS_COUNT: Final = 2 _MAX_PARAM_COLLISION_ATTEMPTS: Final = 1000 @@ -451,7 +459,8 @@ def reset(self) -> None: self._rebind_processor = None self._declared_parameters = () - def _normalize_dialect(self, dialect: "DialectType") -> "str | None": + @staticmethod + def _normalize_dialect(dialect: "DialectType") -> "str | None": """Convert dialect to string representation. Args: @@ -493,7 +502,8 @@ def _init_from_sql_object(self, sql_obj: "SQL") -> None: if sql_obj.is_processed: self._processed_state = sql_obj.get_processed_state() - def _should_auto_detect_many(self, parameters: tuple) -> bool: + @staticmethod + def _should_auto_detect_many(parameters: tuple) -> bool: """Detect execute_many mode from parameter structure. Args: @@ -506,8 +516,6 @@ def _should_auto_detect_many(self, parameters: tuple) -> bool: param_list = parameters[0] if not param_list: return False - # Optimization: Check only the first element for batch structure - # O(1) check instead of O(N) scan first_item = param_list[0] if isinstance(first_item, (tuple, list, dict)): return len(param_list) > 1 @@ -531,7 +539,8 @@ def _process_parameters(self, *parameters: Any, dialect: str | None = None, **kw self._normalize_parameters(parameters) self._named_parameters.update(kwargs) - def _extract_filters(self, parameters: "tuple[Any, ...]") -> "list[StatementFilter]": + @staticmethod + def _extract_filters(parameters: "tuple[Any, ...]") -> "list[StatementFilter]": return [p for p in parameters if is_statement_filter(p)] def _normalize_parameters(self, parameters: "tuple[Any, ...]") -> None: @@ -1094,8 +1103,13 @@ def _create_modified_copy_with_expression(self, new_expr: "exp.Expr") -> "SQL": Returns: New SQL instance with the expression and copied state """ + new_sql = self._clone_base(new_expr) + new_sql._sql_param_counters = self._sql_param_counters.copy() + return new_sql + + def _clone_base(self, statement_seed: "str | exp.Expr") -> "SQL": new_sql = SQL( - new_expr, + statement_seed, *self._original_parameters, statement_config=self._statement_config, is_many=self._is_many, @@ -1104,7 +1118,6 @@ def _create_modified_copy_with_expression(self, new_expr: "exp.Expr") -> "SQL": new_sql._named_parameters.update(self._named_parameters) new_sql._positional_parameters = self._positional_parameters.copy() new_sql._filters = self._filters.copy() - new_sql._sql_param_counters = self._sql_param_counters.copy() return new_sql def add_named_parameter(self, name: str, value: Any) -> "SQL": @@ -1117,21 +1130,9 @@ def add_named_parameter(self, name: str, value: Any) -> "SQL": Returns: New SQL instance with the added parameter """ - original_params = self._original_parameters - config = self._statement_config - is_many = self._is_many statement_seed = self._raw_expression or self._raw_sql - new_sql = SQL( - statement_seed, - *original_params, - statement_config=config, - is_many=is_many, - declared_parameters=self._declared_parameters, - ) - new_sql._named_parameters.update(self._named_parameters) + new_sql = self._clone_base(statement_seed) new_sql._named_parameters[name] = value - new_sql._positional_parameters = self._positional_parameters.copy() - new_sql._filters = self._filters.copy() return new_sql def where(self, condition: "str | exp.Expr") -> "SQL": @@ -1178,14 +1179,7 @@ def where_eq(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_eq) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_eq) def where_neq(self, column: "str | exp.Column", value: Any) -> "SQL": """Add WHERE column != value condition. @@ -1197,14 +1191,7 @@ def where_neq(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_neq) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_neq) def where_lt(self, column: "str | exp.Column", value: Any) -> "SQL": """Add WHERE column < value condition. @@ -1216,14 +1203,7 @@ def where_lt(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_lt) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_lt) def where_lte(self, column: "str | exp.Column", value: Any) -> "SQL": """Add WHERE column <= value condition. @@ -1235,14 +1215,7 @@ def where_lte(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_lte) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_lte) def where_gt(self, column: "str | exp.Column", value: Any) -> "SQL": """Add WHERE column > value condition. @@ -1254,14 +1227,7 @@ def where_gt(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_gt) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_gt) def where_gte(self, column: "str | exp.Column", value: Any) -> "SQL": """Add WHERE column >= value condition. @@ -1273,14 +1239,7 @@ def where_gte(self, column: "str | exp.Column", value: Any) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_gte) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = value - return new_sql + return self._where_comparison(column, value, expr_gte) def where_like(self, column: "str | exp.Column", pattern: str) -> "SQL": """Add WHERE column LIKE pattern condition. @@ -1292,14 +1251,7 @@ def where_like(self, column: "str | exp.Column", pattern: str) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_like) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = pattern - return new_sql + return self._where_comparison(column, pattern, expr_like) def where_ilike(self, column: "str | exp.Column", pattern: str) -> "SQL": """Add WHERE column ILIKE pattern condition (case-insensitive). @@ -1311,14 +1263,7 @@ def where_ilike(self, column: "str | exp.Column", pattern: str) -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - param_name = self._generate_sql_param_name(col_name) - condition = create_condition(column, param_name, expr_ilike) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters[param_name] = pattern - return new_sql + return self._where_comparison(column, pattern, expr_ilike) def where_is_null(self, column: "str | exp.Column") -> "SQL": """Add WHERE column IS NULL condition. @@ -1329,10 +1274,8 @@ def where_is_null(self, column: "str | exp.Column") -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() condition = create_condition(column, "_unused", expr_is_null) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - return self._create_modified_copy_with_expression(new_expr) + return self._where_condition(condition) def where_is_not_null(self, column: "str | exp.Column") -> "SQL": """Add WHERE column IS NOT NULL condition. @@ -1343,10 +1286,8 @@ def where_is_not_null(self, column: "str | exp.Column") -> "SQL": Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() condition = create_condition(column, "_unused", expr_is_not_null) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - return self._create_modified_copy_with_expression(new_expr) + return self._where_condition(condition) def where_in(self, column: "str | exp.Column", values: "Sequence[Any]") -> "SQL": """Add WHERE column IN (values) condition. @@ -1358,27 +1299,7 @@ def where_in(self, column: "str | exp.Column", values: "Sequence[Any]") -> "SQL" Returns: New SQL instance with WHERE condition applied """ - if not values: - expression = self._get_or_parse_expression() - false_condition = exp.EQ(this=exp.Literal.number(1), expression=exp.Literal.number(0)) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, false_condition)) - return self._create_modified_copy_with_expression(new_expr) - - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - - param_names: list[str] = [] - param_values: dict[str, Any] = {} - for i, val in enumerate(values): - param_name = self._generate_sql_param_name(f"{col_name}_in_{i}") - param_names.append(param_name) - param_values[param_name] = val - - condition = create_in_condition(column, param_names) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters.update(param_values) - return new_sql + return self._where_sequence_membership(column, values, "in", create_in_condition, empty_is_false=True) def where_not_in(self, column: "str | exp.Column", values: "Sequence[Any]") -> "SQL": """Add WHERE column NOT IN (values) condition. @@ -1390,24 +1311,7 @@ def where_not_in(self, column: "str | exp.Column", values: "Sequence[Any]") -> " Returns: New SQL instance with WHERE condition applied """ - if not values: - return self - - expression = self._get_or_parse_expression() - col_name = extract_column_name(column) - - param_names: list[str] = [] - param_values: dict[str, Any] = {} - for i, val in enumerate(values): - param_name = self._generate_sql_param_name(f"{col_name}_not_in_{i}") - param_names.append(param_name) - param_values[param_name] = val - - condition = create_not_in_condition(column, param_names) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) - new_sql._named_parameters.update(param_values) - return new_sql + return self._where_sequence_membership(column, values, "not_in", create_not_in_condition, empty_is_false=False) def where_between(self, column: "str | exp.Column", low: Any, high: Any) -> "SQL": """Add WHERE column BETWEEN low AND high condition. @@ -1420,17 +1324,61 @@ def where_between(self, column: "str | exp.Column", low: Any, high: Any) -> "SQL Returns: New SQL instance with WHERE condition applied """ - expression = self._get_or_parse_expression() col_name = extract_column_name(column) low_param = self._generate_sql_param_name(f"{col_name}_low") high_param = self._generate_sql_param_name(f"{col_name}_high") condition = create_between_condition(column, low_param, high_param) - new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) - new_sql = self._create_modified_copy_with_expression(new_expr) + new_sql = self._where_condition(condition) new_sql._named_parameters[low_param] = low new_sql._named_parameters[high_param] = high return new_sql + def _where_condition(self, condition: exp.Expr) -> "SQL": + expression = self._get_or_parse_expression() + new_expr = safe_modify_with_cte(expression, lambda e: apply_where(e, condition)) + return self._create_modified_copy_with_expression(new_expr) + + def _where_comparison( + self, + column: "str | exp.Column", + value: Any, + condition_factory: "Callable[[exp.Expr, exp.Placeholder], exp.Expr]", + ) -> "SQL": + col_name = extract_column_name(column) + param_name = self._generate_sql_param_name(col_name) + condition = create_condition(column, param_name, condition_factory) + new_sql = self._where_condition(condition) + new_sql._named_parameters[param_name] = value + return new_sql + + def _where_sequence_membership( + self, + column: "str | exp.Column", + values: "Sequence[Any]", + parameter_suffix: str, + condition_factory: "Callable[[str | exp.Column, list[str]], exp.Expr]", + *, + empty_is_false: bool, + ) -> "SQL": + if not values: + if not empty_is_false: + return self + false_condition = exp.EQ(this=exp.Literal.number(1), expression=exp.Literal.number(0)) + return self._where_condition(false_condition) + + col_name = extract_column_name(column) + param_names: list[str] = [] + param_values: dict[str, Any] = {} + for index, value in enumerate(values): + param_name = self._generate_sql_param_name(f"{col_name}_{parameter_suffix}_{index}") + param_names.append(param_name) + param_values[param_name] = value + + condition = condition_factory(column, param_names) + new_sql = self._where_condition(condition) + new_sql._named_parameters.update(param_values) + return new_sql + def order_by(self, *items: "str | exp.Expr", desc: bool = False) -> "SQL": """Add ORDER BY clause to the SQL statement. diff --git a/sqlspec/core/type_converter.py b/sqlspec/core/type_converter.py index f1c6a7890..657ea4d67 100644 --- a/sqlspec/core/type_converter.py +++ b/sqlspec/core/type_converter.py @@ -109,8 +109,6 @@ def convert_json(value: str) -> "Any": Returns: Decoded Python object. """ - # Keep the hot coercion path in this compiled module instead of bouncing - # through the interpreted serializer-selection shell. return json.loads(value) @@ -224,7 +222,7 @@ def detect_type(self, value: str) -> str | None: if not match: return None - return next((key for key, match_value in match.groupdict().items() if match_value), None) + return match.lastgroup def convert_value(self, value: str, detected_type: str) -> "Any": """Convert string value to appropriate Python type. diff --git a/sqlspec/dialects/spanner/_spanner.py b/sqlspec/dialects/spanner/_spanner.py index c3d0b1be5..da3b31f68 100644 --- a/sqlspec/dialects/spanner/_spanner.py +++ b/sqlspec/dialects/spanner/_spanner.py @@ -29,6 +29,11 @@ class Spanner(BigQuery): Generator = SpannerGenerator + class Tokenizer(BigQuery.Tokenizer): + """Tokenizer for Spanner GoogleSQL string literal escapes.""" + + STRING_ESCAPES = ["'", "\\"] + def parse(self, sql: str, **opts: Any) -> "list[exp.Expr | None]": """Repair CREATE TABLE statements that sqlglot still falls back to Command for.""" expressions = super().parse(sql, **opts) diff --git a/sqlspec/extensions/flask/extension.py b/sqlspec/extensions/flask/extension.py index 180e06964..9a927f29a 100644 --- a/sqlspec/extensions/flask/extension.py +++ b/sqlspec/extensions/flask/extension.py @@ -211,6 +211,9 @@ def _before_request_handler(self) -> None: Stores connection in Flask g object for each configured database. Also stores context managers for proper cleanup. Extracts correlation ID if correlation middleware is enabled. + + Note: + Imports flask components dynamically at runtime to avoid hard dependency. """ from flask import current_app, g, request @@ -250,6 +253,9 @@ def _before_request_handler(self) -> None: def _after_request_handler(self, response: "Response") -> "Response": """Handle transaction after request based on response status. + Note: + Imports flask components dynamically at runtime to avoid hard dependency. + Args: response: Flask response object. @@ -288,6 +294,9 @@ def _teardown_appcontext_handler(self, _exc: "BaseException | None" = None) -> N Closes all connections, cleans up g object, and clears correlation context. + Note: + Imports flask components dynamically at runtime to avoid hard dependency. + Args: _exc: Exception that occurred (if any). """ @@ -359,6 +368,9 @@ def get_session(self, key: "str | None" = None) -> Any: def get_connection(self, key: "str | None" = None) -> Any: """Get database connection for current request. + Note: + Imports flask components dynamically at runtime to avoid hard dependency. + Args: key: Session key for multi-database configs. Defaults to first config if None. diff --git a/sqlspec/extensions/litestar/plugin.py b/sqlspec/extensions/litestar/plugin.py index d410bb566..23aec395a 100644 --- a/sqlspec/extensions/litestar/plugin.py +++ b/sqlspec/extensions/litestar/plugin.py @@ -28,6 +28,7 @@ get_sqlspec_scope_state, set_sqlspec_scope_state, ) +from sqlspec.extensions.litestar.cli import database_group from sqlspec.extensions.litestar.handlers import ( autocommit_handler_maker, connection_provider_maker, @@ -327,8 +328,6 @@ def on_cli_init(self, cli: "Group") -> None: Args: cli: The Click command group to add commands to. """ - from sqlspec.extensions.litestar.cli import database_group - cli.add_command(database_group) def on_app_init(self, app_config: "AppConfig") -> "AppConfig": @@ -893,13 +892,17 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No attrs: dict[str, str] = {"route": scope.get("path", ""), "framework": "litestar"} handler = scope.get("route_handler") - if handler is not None: - fn = getattr(handler, "fn", None) + if handler is not None and hasattr(handler, "fn") and hasattr(handler, "owner"): + fn = handler.fn if fn is not None: - attrs["action"] = getattr(fn, "__name__", "") - owner = getattr(handler, "owner", None) + fn_name = getattr(fn, "__name__", None) + if isinstance(fn_name, str): + attrs["action"] = fn_name + owner = handler.owner if owner is not None: - attrs["controller"] = getattr(owner, "__name__", "") + owner_name = getattr(owner, "__name__", None) + if isinstance(owner_name, str): + attrs["controller"] = owner_name previous = SQLCommenterContext.get() SQLCommenterContext.set(attrs) diff --git a/sqlspec/extensions/sanic/extension.py b/sqlspec/extensions/sanic/extension.py index 835ff87f3..ff31c8bea 100644 --- a/sqlspec/extensions/sanic/extension.py +++ b/sqlspec/extensions/sanic/extension.py @@ -13,6 +13,7 @@ pop_context_value, set_context_value, ) +from sqlspec.protocols import HasNameProtocol from sqlspec.utils.correlation import CorrelationContext from sqlspec.utils.logging import get_logger, log_with_context from sqlspec.utils.sync_tools import ensure_async_, with_ensure_async_ @@ -371,15 +372,13 @@ def _request_action(self, request: Any) -> str | None: endpoint = getattr(request, "endpoint", None) if isinstance(endpoint, str) and endpoint: return endpoint.rsplit(".", 1)[-1] - endpoint_name = getattr(endpoint, "__name__", None) - if isinstance(endpoint_name, str) and endpoint_name: - return endpoint_name + if isinstance(endpoint, HasNameProtocol): + return endpoint.__name__ route = getattr(request, "route", None) handler = getattr(route, "handler", None) - handler_name = getattr(handler, "__name__", None) - if isinstance(handler_name, str) and handler_name: - return handler_name + if isinstance(handler, HasNameProtocol): + return handler.__name__ name = getattr(request, "name", None) if isinstance(name, str) and name: diff --git a/sqlspec/extensions/starlette/extension.py b/sqlspec/extensions/starlette/extension.py index 5213702ab..b8113d6f1 100644 --- a/sqlspec/extensions/starlette/extension.py +++ b/sqlspec/extensions/starlette/extension.py @@ -8,6 +8,7 @@ from sqlspec.extensions.starlette._utils import get_or_create_session, get_state_value from sqlspec.extensions.starlette.middleware import ( CorrelationMiddleware, + SQLCommenterMiddleware, SQLSpecAutocommitMiddleware, SQLSpecManualMiddleware, ) @@ -250,8 +251,6 @@ def _add_sqlcommenter_middleware(self, app: "Starlette") -> None: for config_state in self._config_states: if config_state.enable_sqlcommenter_middleware and config_state.config.statement_config.enable_sqlcommenter: - from sqlspec.extensions.starlette.middleware import SQLCommenterMiddleware - app.add_middleware(SQLCommenterMiddleware, framework=config_state.sqlcommenter_framework) self._sqlcommenter_middleware_added = True log_with_context( diff --git a/sqlspec/extensions/starlette/middleware.py b/sqlspec/extensions/starlette/middleware.py index c0d3107f1..7dacbcbb5 100644 --- a/sqlspec/extensions/starlette/middleware.py +++ b/sqlspec/extensions/starlette/middleware.py @@ -6,6 +6,7 @@ from sqlspec.core import CorrelationExtractor from sqlspec.core.sqlcommenter import SQLCommenterContext from sqlspec.extensions.starlette._utils import get_state_value, pop_state_value, set_state_value +from sqlspec.protocols import HasNameProtocol from sqlspec.utils.correlation import CorrelationContext from sqlspec.utils.sync_tools import ensure_async_, with_ensure_async_ @@ -260,7 +261,7 @@ async def dispatch(self, request: "Request", call_next: Any) -> "Response": """ attrs: dict[str, str] = {"route": request.url.path, "framework": self._framework} endpoint = request.scope.get("endpoint") - if endpoint is not None and hasattr(endpoint, "__name__"): + if isinstance(endpoint, HasNameProtocol): attrs["action"] = endpoint.__name__ previous = SQLCommenterContext.get() diff --git a/sqlspec/protocols.py b/sqlspec/protocols.py index 36e9a864d..3b67744a6 100644 --- a/sqlspec/protocols.py +++ b/sqlspec/protocols.py @@ -35,6 +35,7 @@ "HasConnectionConfigProtocol", "HasDataProtocol", "HasDatabaseUrlAndBindKeyProtocol", + "HasErrnoProtocol", "HasErrorsProtocol", "HasExecuteProtocol", "HasExpressionAndParametersProtocol", @@ -62,6 +63,7 @@ "HasTypecodeSizedProtocol", "HasValueProtocol", "HasWhereProtocol", + "LitestarRouteHandlerProtocol", "MappingLikeProtocol", "NotificationProtocol", "ObjectStoreProtocol", @@ -226,6 +228,13 @@ class HasErrorsProtocol(Protocol): errors: "list[dict[str, Any]] | None" +@runtime_checkable +class HasErrnoProtocol(Protocol): + """Protocol for objects exposing errno details.""" + + errno: int | None + + @runtime_checkable class HasDataProtocol(Protocol): """Protocol for results exposing a data attribute.""" @@ -247,6 +256,14 @@ class HasNameProtocol(Protocol): __name__: str +@runtime_checkable +class LitestarRouteHandlerProtocol(Protocol): + """Protocol for Litestar route handlers.""" + + fn: object + owner: object + + @runtime_checkable class HasNotifiesProtocol(Protocol): """Protocol for asyncpg-like connections exposing notifications.""" diff --git a/sqlspec/storage/backends/fsspec.py b/sqlspec/storage/backends/fsspec.py index b0ceb6253..5b54b4a41 100644 --- a/sqlspec/storage/backends/fsspec.py +++ b/sqlspec/storage/backends/fsspec.py @@ -16,6 +16,8 @@ from sqlspec.utils.sync_tools import async_ if TYPE_CHECKING: + from fsspec import AbstractFileSystem + from sqlspec.typing import ArrowRecordBatch, ArrowTable __all__ = ("FSSpecBackend",) @@ -36,6 +38,8 @@ class FSSpecBackend: __slots__ = ("_fs_uri", "base_path", "fs", "protocol") backend_type: ClassVar[str] = "fsspec" + if TYPE_CHECKING: + fs: AbstractFileSystem def __init__(self, uri: str, **kwargs: Any) -> None: """Initialize the fsspec-backed storage backend. @@ -302,7 +306,7 @@ def list_objects_sync(self, prefix: str = "", recursive: bool = True, **kwargs: def glob_sync(self, pattern: str, **kwargs: Any) -> "list[str]": """Find objects matching a glob pattern synchronously.""" resolved_pattern = resolve_storage_path(pattern, self.base_path, self.protocol, strip_file_scheme=False) - results = sorted(self.fs.glob(resolved_pattern, **kwargs)) # pyright: ignore + results = cast("list[str]", sorted(self.fs.glob(resolved_pattern, **kwargs))) _log_storage_event( "storage.list", backend_type=self.backend_type, @@ -394,7 +398,7 @@ def stream_read_sync(self, path: "str | Path", chunk_size: "int | None" = None, chunk = f.read(chunk_size) if not chunk: break - yield chunk + yield cast("bytes", chunk) def stream_arrow_sync(self, pattern: str, **kwargs: Any) -> Iterator["ArrowRecordBatch"]: """Stream Arrow record batches from storage synchronously. diff --git a/sqlspec/storage/backends/obstore.py b/sqlspec/storage/backends/obstore.py index 8e9b08ac7..8f79882a1 100644 --- a/sqlspec/storage/backends/obstore.py +++ b/sqlspec/storage/backends/obstore.py @@ -11,7 +11,7 @@ from datetime import timedelta from functools import partial from pathlib import Path, PurePosixPath -from typing import Any, ClassVar, Final, cast, overload +from typing import TYPE_CHECKING, Any, ClassVar, Final, cast, overload from urllib.parse import urlparse from mypy_extensions import mypyc_attr @@ -20,6 +20,9 @@ from sqlspec.storage._paths import is_file_destination, resolve_storage_path from sqlspec.storage._utils import _log_storage_event, import_pyarrow, import_pyarrow_parquet from sqlspec.storage.backends.base import AsyncArrowBatchIterator, AsyncObStoreStreamIterator + +if TYPE_CHECKING: + from obstore.store import ObjectStore from sqlspec.storage.errors import execute_sync_storage_operation from sqlspec.typing import ArrowRecordBatch, ArrowTable from sqlspec.utils.module_loader import ensure_obstore @@ -41,6 +44,18 @@ class ObStoreBackend: local filesystem, and HTTP endpoints. All synchronous methods use the *_sync suffix for consistency with async methods. + + Implementation Details & Invariants: + - LocalStore Paths: For LocalStore, the base_path is already included in the store + root (combined with the URI path; if base_path is absolute, Path division will + use it directly). Hence, we use an empty prefix when resolving paths for LocalStore, + whereas cloud stores use base_path as a prefix. + - Native Streaming: Uses obstore's native streaming yielding Buffer objects, which + are converted to bytes. + - Seekable Streams: PyArrow's ParquetFile requires a seekable file, so we wrap + the buffered stream accordingly (e.g. using io.BytesIO). + - Thread Offloading: Uses async_() with a storage limiter to offload blocking + PyArrow serialization/parsing to a thread pool, preventing event loop blocking. """ __slots__ = ("_is_local_store", "_local_store_root", "base_path", "protocol", "store", "store_options", "store_uri") @@ -71,7 +86,7 @@ def __init__(self, uri: str, **kwargs: Any) -> None: self.store_uri = uri self.base_path = base_path.rstrip("/") if base_path else "" self.store_options = kwargs - self.store: Any + self.store: ObjectStore | Any self._is_local_store = False self._local_store_root = "" self.protocol = uri.split("://", 1)[0] if "://" in uri else "file" @@ -92,8 +107,6 @@ def __init__(self, uri: str, **kwargs: Any) -> None: if is_file_destination(path_obj): path_str = str(path_obj.parent) - # Combine URI path with base_path for correct storage location - # If base_path is absolute, Path division will use it directly (backward compat) local_store_root_obj = Path(path_str) if self.base_path: local_store_root_obj /= self.base_path @@ -209,8 +222,6 @@ def write_text_sync(self, path: "str | Path", data: str, encoding: str = "utf-8" def list_objects_sync(self, prefix: str = "", recursive: bool = True, **kwargs: Any) -> "list[str]": # pyright: ignore[reportUnusedParameter] """List objects using obstore synchronously.""" - # For LocalStore, the base_path is already included in the store root, - # so we use empty prefix when none is given. For cloud stores, use base_path. if prefix: resolved_prefix = resolve_storage_path(prefix, self.base_path, self.protocol, strip_file_scheme=True) elif self._is_local_store: @@ -357,7 +368,6 @@ def get_metadata_sync(self, path: "str | Path", **kwargs: Any) -> "dict[str, obj """Get object metadata using obstore synchronously.""" resolved_path = self._resolve_path(path) - # Keep in sync with get_metadata_async. try: metadata = self.store.head(resolved_path) except Exception: @@ -371,8 +381,9 @@ def get_metadata_sync(self, path: "str | Path", **kwargs: Any) -> "dict[str, obj "e_tag": metadata.get("e_tag"), "version": metadata.get("version"), } - if metadata.get("metadata"): - result["custom_metadata"] = metadata["metadata"] + metadata_dict = cast("dict[str, Any]", metadata) + if custom_metadata := metadata_dict.get("metadata"): + result["custom_metadata"] = custom_metadata return result def is_object_sync(self, path: "str | Path") -> bool: @@ -475,10 +486,8 @@ def stream_read_sync(self, path: "str | Path", chunk_size: "int | None" = None, path=resolved_path, ) - # Use obstore's native streaming - yields Buffer objects - # GetResult.stream(min_chunk_size) returns an iterator of chunks for chunk in result.stream(min_chunk_size=chunk_size): - yield bytes(chunk) # Convert Buffer to bytes + yield bytes(chunk) def stream_arrow_sync(self, pattern: str, **kwargs: Any) -> Iterator[ArrowRecordBatch]: """Stream Arrow record batches using obstore's native streaming synchronously. @@ -499,8 +508,6 @@ def stream_arrow_sync(self, pattern: str, **kwargs: Any) -> Iterator[ArrowRecord path=resolved_path, ) - # Create a file-like object that streams from obstore - # PyArrow's ParquetFile needs a seekable file, so we buffer the stream buffer = io.BytesIO() for chunk in result.stream(): buffer.write(chunk) @@ -633,8 +640,6 @@ async def stream_read_async( async def list_objects_async(self, prefix: str = "", recursive: bool = True, **kwargs: Any) -> "list[str]": # pyright: ignore[reportUnusedParameter] """List objects in storage asynchronously.""" - # For LocalStore, the base_path is already included in the store root, - # so we use empty prefix when none is given. For cloud stores, use base_path. if prefix: resolved_prefix = resolve_storage_path(prefix, self.base_path, self.protocol, strip_file_scheme=True) elif self._is_local_store: @@ -768,7 +773,6 @@ async def get_metadata_async(self, path: "str | Path", **kwargs: Any) -> "dict[s resolved_path = resolve_storage_path(path, self.base_path, self.protocol, strip_file_scheme=True) result: dict[str, object] = {} - # Keep in sync with get_metadata_sync. try: metadata = await self.store.head_async(resolved_path) result.update({ @@ -779,8 +783,9 @@ async def get_metadata_async(self, path: "str | Path", **kwargs: Any) -> "dict[s "e_tag": metadata.get("e_tag"), "version": metadata.get("version"), }) - if metadata.get("metadata"): - result["custom_metadata"] = metadata["metadata"] + metadata_dict = cast("dict[str, Any]", metadata) + if custom_metadata := metadata_dict.get("metadata"): + result["custom_metadata"] = custom_metadata except Exception: return {"path": resolved_path, "exists": False} @@ -796,7 +801,6 @@ async def read_arrow_async(self, path: "str | Path", **kwargs: Any) -> ArrowTabl resolved_path = self._resolve_path(path) data = await self._read_bytes_resolved_async(resolved_path) - # Offload PyArrow parsing to thread pool result = await async_(pq.read_table)(io.BytesIO(data), **kwargs) _log_storage_event( diff --git a/tests/conftest.py b/tests/conftest.py index a2f03520f..71a7bb032 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,8 @@ import pytest +from sqlspec.core.cache import clear_all_caches + warnings.filterwarnings( "ignore", message="You are using a Python version.*which Google will stop supporting", category=FutureWarning ) @@ -147,8 +149,6 @@ def disable_sync_to_thread_warning(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture(autouse=True) def clear_sql_caches() -> Generator[None, None, None]: """Clear SQL caches before each test to ensure isolation.""" - from sqlspec.core.cache import clear_all_caches - clear_all_caches() yield clear_all_caches() diff --git a/tests/integration/adapters/oracledb/test_smart_lob_coercion.py b/tests/integration/adapters/oracledb/test_smart_lob_coercion.py index c581d18b9..32c8ac9bc 100644 --- a/tests/integration/adapters/oracledb/test_smart_lob_coercion.py +++ b/tests/integration/adapters/oracledb/test_smart_lob_coercion.py @@ -151,8 +151,8 @@ async def test_json_bytes_payload_no_manual_createlob_needed(oracle_async_sessio Replicates the workaround at oracledb-vertexai-demo:utils/fixtures.py:282-286 where a manual ``await connection.createlob(...)`` was needed because raw - bytes hit the BLOB-coercion fallback. With C2's wrapper-aware routing and - C1's native JSON binding, the raw bytes path is no longer needed — but the + bytes hit the BLOB-coercion fallback. With wrapper-aware routing and + native JSON binding, the raw bytes path is no longer needed — but the user-facing ergonomic answer is to wrap with ``OracleJson`` so the handler chain claims it cleanly. """ diff --git a/tests/unit/adapters/test_asyncpg/test_driver.py b/tests/unit/adapters/test_asyncpg/test_driver.py index 527c485da..c37ec1242 100644 --- a/tests/unit/adapters/test_asyncpg/test_driver.py +++ b/tests/unit/adapters/test_asyncpg/test_driver.py @@ -94,7 +94,7 @@ async def test_asyncpg_copy_from_stdin_uses_metadata_table_fallback() -> None: config = default_statement_config.replace( execution_args={"postgres_copy_data": "1", "postgres_copy_table": "public.users"} ) - statement = SimpleNamespace(operation_type="COPY_FROM", statement_config=config) + statement = SimpleNamespace(operation_type="COPY_FROM", statement_config=config, is_processed=False) cursor = _CopyCursor() driver = _CompiledCopyDriver(connection=_connection()) @@ -153,7 +153,7 @@ def get_compiled_sql(*_args: object, **_kwargs: object) -> tuple[str, object]: @pytest.mark.anyio async def test_asyncpg_copy_from_stdin_requires_table_name() -> None: config = default_statement_config.replace(execution_args={"postgres_copy_data": "1"}) - statement = SimpleNamespace(operation_type="COPY_FROM", statement_config=config) + statement = SimpleNamespace(operation_type="COPY_FROM", statement_config=config, is_processed=False) cursor = _CopyCursor() driver = _CompiledCopyDriver(connection=_connection()) diff --git a/tests/unit/adapters/test_mssql_python/test_data_dictionary.py b/tests/unit/adapters/test_mssql_python/test_data_dictionary.py index 7fc4e3bdc..0ff10b017 100644 --- a/tests/unit/adapters/test_mssql_python/test_data_dictionary.py +++ b/tests/unit/adapters/test_mssql_python/test_data_dictionary.py @@ -1,7 +1,7 @@ """Unit tests for the mssql_python data dictionary.""" from pathlib import Path -from typing import Any +from typing import Any, cast import pytest @@ -69,7 +69,7 @@ def test_sync_data_dictionary_builds_version_info() -> None: driver = FakeSyncDriver() data_dictionary = MssqlPythonSyncDataDictionary() - version = data_dictionary.get_version(driver) + version = data_dictionary.get_version(cast(Any, driver)) assert isinstance(version, MssqlVersionInfo) assert version.major == 16 @@ -77,8 +77,8 @@ def test_sync_data_dictionary_builds_version_info() -> None: assert version.revision == 2 assert version.edition == "Developer Edition" assert version.is_azure_sql is False - assert data_dictionary.get_feature_flag(driver, "supports_greatest_least") is True - assert data_dictionary.get_feature_flag(driver, "supports_native_json") is False + assert data_dictionary.get_feature_flag(cast(Any, driver), "supports_greatest_least") is True + assert data_dictionary.get_feature_flag(cast(Any, driver), "supports_native_json") is False def test_mssql_version_info_uses_build_in_version_tuple_not_patch() -> None: @@ -94,7 +94,7 @@ def test_sync_data_dictionary_merges_table_lists_with_default_schema() -> None: driver = FakeSyncDriver() data_dictionary = MssqlPythonSyncDataDictionary() - tables = data_dictionary.get_tables(driver) + tables = data_dictionary.get_tables(cast(Any, driver)) assert tables == [{"schema_name": "dbo", "table_name": "parent"}, {"schema_name": "dbo", "table_name": "orphan"}] assert driver.select_calls[0][1]["schema_name"] == "dbo" @@ -106,7 +106,7 @@ def test_sync_data_dictionary_selects_columns_by_table() -> None: driver = FakeSyncDriver() data_dictionary = MssqlPythonSyncDataDictionary() - columns = data_dictionary.get_columns(driver, table="app", schema="custom") + columns = data_dictionary.get_columns(cast(Any, driver), table="app", schema="custom") assert columns == [{"column_name": "id"}] assert driver.select_calls[0][1]["schema_name"] == "custom" @@ -119,12 +119,12 @@ async def test_async_data_dictionary_builds_azure_version_info() -> None: driver = FakeAsyncDriver() data_dictionary = MssqlPythonAsyncDataDictionary() - version = await data_dictionary.get_version(driver) + version = await data_dictionary.get_version(cast(Any, driver)) assert isinstance(version, MssqlVersionInfo) assert version.major == 12 assert version.is_azure_sql is True - assert await data_dictionary.get_feature_flag(driver, "supports_native_json") is True + assert await data_dictionary.get_feature_flag(cast(Any, driver), "supports_native_json") is True @pytest.mark.anyio @@ -133,7 +133,7 @@ async def test_async_data_dictionary_selects_indexes_by_table() -> None: driver = FakeAsyncDriver() data_dictionary = MssqlPythonAsyncDataDictionary() - indexes = await data_dictionary.get_indexes(driver, table="app") + indexes = await data_dictionary.get_indexes(cast(Any, driver), table="app") assert indexes == [{"index_name": "ix_app_id", "table_name": "app", "columns": "id"}] assert driver.select_calls[0][1]["schema_name"] == "dbo" diff --git a/tests/unit/adapters/test_psqlpy/test_config.py b/tests/unit/adapters/test_psqlpy/test_config.py index bcf593c04..3fb5806d4 100644 --- a/tests/unit/adapters/test_psqlpy/test_config.py +++ b/tests/unit/adapters/test_psqlpy/test_config.py @@ -76,6 +76,22 @@ def test_psqlpy_config_preserves_cast_detection_override() -> None: assert config.driver_features["enable_cast_detection"] is False +def test_psqlpy_runtime_aliases_resolve_to_installed_classes() -> None: + """Psqlpy public runtime aliases should expose installed psqlpy classes.""" + psqlpy = pytest.importorskip("psqlpy") + from sqlspec.adapters.psqlpy import PsqlpyConnection as PublicPsqlpyConnection + from sqlspec.adapters.psqlpy._typing import PsqlpyListener + + namespace = PsqlpyConfig().get_signature_namespace() + + assert PsqlpyConnection is psqlpy.Connection + assert PublicPsqlpyConnection is psqlpy.Connection + assert PsqlpyListener is psqlpy.Listener + assert PsqlpyConfig.connection_type is psqlpy.Connection + assert namespace["PsqlpyConnection"] is psqlpy.Connection + assert isinstance(object(), PsqlpyConnection) is False + + def test_psqlpy_build_postgres_extension_probe_names_filters_disabled_features() -> None: """Only enabled extension probes should be returned.""" assert build_postgres_extension_probe_names({"enable_pgvector": True, "enable_paradedb": False}) == ["vector"] diff --git a/tests/unit/adapters/test_spanner/test_config.py b/tests/unit/adapters/test_spanner/test_config.py index 9775b9002..9daf7e9f5 100644 --- a/tests/unit/adapters/test_spanner/test_config.py +++ b/tests/unit/adapters/test_spanner/test_config.py @@ -4,7 +4,6 @@ import pytest from google.cloud.spanner_v1.pool import AbstractSessionPool, BurstyPool, FixedSizePool -from sqlspec.adapters.spanner import config as config_module from sqlspec.adapters.spanner.config import ( SpannerConnectionParams, SpannerDriverFeatures, @@ -167,7 +166,9 @@ def instance(self, instance_id: str, **kwargs: Any) -> _FakeInstance: return instance created_clients: list[_FakeClient] = [] - monkeypatch.setattr(config_module, "Client", _FakeClient) + import google.cloud.spanner_v1 + + monkeypatch.setattr(google.cloud.spanner_v1, "Client", _FakeClient) client_info = object() query_options = object() diff --git a/tests/unit/config/test_storage_capabilities.py b/tests/unit/config/test_storage_capabilities.py index 9bf7db709..b6246727f 100644 --- a/tests/unit/config/test_storage_capabilities.py +++ b/tests/unit/config/test_storage_capabilities.py @@ -1,8 +1,10 @@ +import inspect from contextlib import AbstractContextManager, asynccontextmanager, contextmanager from typing import TYPE_CHECKING, Any import pytest +import sqlspec.core.config_runtime as config_runtime_module from sqlspec.config import AsyncDatabaseConfig, NoPoolAsyncConfig, NoPoolSyncConfig, SyncDatabaseConfig from sqlspec.core import StatementConfig from sqlspec.core.config_runtime import ( @@ -27,6 +29,13 @@ pytestmark = requires_interpreted +def test_config_runtime_uses_direct_core_submodule_imports() -> None: + source = inspect.getsource(config_runtime_module) + assert "from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig" in source + assert "from sqlspec.core.statement import StatementConfig" in source + assert "from sqlspec.core import ParameterStyle, ParameterStyleConfig, StatementConfig" not in source + + if TYPE_CHECKING: _NoPoolSyncConfigBase = NoPoolSyncConfig[Any, "_DummyDriver"] _NoPoolAsyncConfigBase = NoPoolAsyncConfig[Any, "_AsyncDummyDriver"] diff --git a/tests/unit/core/test_cache.py b/tests/unit/core/test_cache.py index 675efe26b..1d45d359b 100644 --- a/tests/unit/core/test_cache.py +++ b/tests/unit/core/test_cache.py @@ -25,6 +25,8 @@ import pytest import sqlspec.core.cache as cache_module +import sqlspec.core.hashing as hashing_module +import sqlspec.core.pipeline as pipeline_module from sqlspec.core import ( CacheConfig, CacheKey, @@ -42,6 +44,21 @@ ) +def test_cache_hash_pipeline_exports_are_additive() -> None: + """Public export alignment is additive for documented helper surfaces.""" + for name in ( + "CacheConfig", + "clear_all_caches", + "get_cache_statistics", + "log_cache_stats", + "reset_stats_only", + "update_cache_config", + ): + assert name in cache_module.__all__ + assert "hash_filters" in hashing_module.__all__ + assert "configure_statement_pipeline_cache" in pipeline_module.__all__ + + def test_cache_key_creation_and_immutability() -> None: """Test CacheKey creation and immutable behavior.""" key_data = ("test", "key", 123) @@ -180,6 +197,16 @@ def test_lru_cache_initialization() -> None: assert len(cache) == 0 +def test_lru_cache_uses_non_reentrant_lock() -> None: + """LRUCache methods should not depend on re-entrant locking.""" + cache = LRUCache() + source = inspect.getsource(LRUCache.__init__) + + assert isinstance(cache._lock, type(threading.Lock())) + assert "threading.Lock()" in source + assert "threading.RLock()" not in source + + def test_lru_cache_basic_operations() -> None: """Test basic cache operations - get, put, delete.""" cache = LRUCache(max_size=3) @@ -389,6 +416,55 @@ def test_namespaced_cache_additional_namespaces() -> None: assert cache.get_file(cache_key) is file_value +def test_namespaced_cache_delete_external_api_smoke() -> None: + """Deletion helpers are external API and delete each namespace key.""" + cache = NamespacedCache(CacheConfig()) + namespaces = ("statement", "expression", "optimized", "builder", "file") + + for namespace in namespaces: + put = getattr(cache, f"put_{namespace}") + get = getattr(cache, f"get_{namespace}") + delete = getattr(cache, f"delete_{namespace}") + + put("stable-key", f"{namespace}-value", "sqlite") + assert get("stable-key", "sqlite") == f"{namespace}-value" + assert delete("stable-key", "sqlite") is True + assert get("stable-key", "sqlite") is None + assert delete("stable-key", "sqlite") is False + + +def test_filters_view_external_api_smoke() -> None: + """FiltersView methods are external API and remain callable.""" + filters = [ + cache_module.Filter("status", "eq", "active"), + cache_module.Filter("status", "neq", "deleted"), + cache_module.Filter("name", "like", "a"), + ] + view = cache_module.FiltersView(filters) + + assert len(view) == 3 + assert list(view) == filters + assert view.get_by_field("status") == filters[:2] + assert view.has_field("name") is True + assert view.has_field("missing") is False + assert len(view.to_canonical()) == 3 + + +def test_cache_external_api_docstring_markers() -> None: + """Dead-code policy markers identify externally callable cache helpers.""" + for member in ( + NamespacedCache.delete_statement, + NamespacedCache.delete_expression, + NamespacedCache.delete_optimized, + NamespacedCache.delete_builder, + NamespacedCache.delete_file, + cache_module.FiltersView.get_by_field, + cache_module.FiltersView.has_field, + cache_module.FiltersView.to_canonical, + ): + assert "External/extension API" in (inspect.getdoc(member) or "") + + def test_namespaced_cache_respects_config_flags() -> None: """Test that cache config flags disable namespaces.""" original_config = get_cache_config() @@ -804,6 +880,15 @@ def test_lru_cache_logs_include_namespace(caplog: pytest.LogCaptureFixture) -> N assert "cache_size" in record.__dict__["extra_fields"] +def test_lru_cache_get_logs_outside_lock() -> None: + """LRUCache.get should not log or check debug state while holding the lock.""" + source = inspect.getsource(LRUCache.get) + lock_body = source.split("with self._lock:", 1)[1].split("if log_event is not None:", 1)[0] + + assert "logger.isEnabledFor" not in lock_body + assert "log_with_context" not in lock_body + + def test_lru_cache_logs_hit_with_namespace(caplog: pytest.LogCaptureFixture) -> None: """Test that cache hit logs include namespace.""" import logging diff --git a/tests/unit/core/test_compiler.py b/tests/unit/core/test_compiler.py index afec2dd6f..21281f0c6 100644 --- a/tests/unit/core/test_compiler.py +++ b/tests/unit/core/test_compiler.py @@ -20,6 +20,7 @@ import time from collections import OrderedDict from datetime import datetime +from pathlib import Path from typing import Any, get_args from unittest.mock import Mock, patch @@ -29,6 +30,7 @@ from sqlglot.errors import ParseError from sqlspec.core import ( + SQL, CompiledSQL, OperationProfile, OperationType, @@ -42,8 +44,9 @@ is_copy_operation, is_copy_to_operation, ) +from sqlspec.core.parameters import structural_fingerprint from sqlspec.core.parameters._processor import _make_cache_key_tuple -from sqlspec.core.pipeline import compile_with_pipeline, reset_statement_pipeline_cache +from sqlspec.core.pipeline import StatementPipelineRegistry, compile_with_pipeline, reset_statement_pipeline_cache from sqlspec.core.statement import get_default_config from tests.conftest import requires_interpreted @@ -324,37 +327,37 @@ def test_cache_key_generation(basic_statement_config: "StatementConfig") -> None Note: SQLSpec uses structural fingerprinting for parameters, meaning cache keys are based on parameter STRUCTURE (types, keys) not VALUES. Same SQL with same parameter structure produces the same cache key regardless of actual values. - """ - from sqlspec.core.parameters import structural_fingerprint + This test verifies: + - Same SQL and parameter structure yields the same cache key. + - Different SQL yields a different key. + - Same SQL with same parameter structure (list of one int) yields the same key. + - Different parameter structure (e.g. dict vs list) yields a different key. + - Different parameter type signature yields a different key. + - Cache keys are tuples for better performance. + """ processor = SQLProcessor(basic_statement_config) - # _make_cache_key expects a precomputed fingerprint, not raw params - # Same SQL and parameter structure = same key fp1 = structural_fingerprint([123]) key1 = processor._make_cache_key("SELECT * FROM users", fp1) key2 = processor._make_cache_key("SELECT * FROM users", fp1) assert key1 == key2 - # Different SQL = different key key3 = processor._make_cache_key("SELECT * FROM posts", fp1) assert key1 != key3 - # Same SQL with same parameter STRUCTURE (list of one int) = SAME key (structural fingerprinting) fp4 = structural_fingerprint([456]) key4 = processor._make_cache_key("SELECT * FROM users", fp4) - assert key1 == key4 # Structural fingerprinting: same structure = same key + assert key1 == key4 - # Different parameter STRUCTURE = different key - fp5 = structural_fingerprint({"id": 123}) # dict vs list + fp5 = structural_fingerprint({"id": 123}) key5 = processor._make_cache_key("SELECT * FROM users", fp5) assert key1 != key5 - fp6 = structural_fingerprint([123, "extra"]) # different type signature + fp6 = structural_fingerprint([123, "extra"]) key6 = processor._make_cache_key("SELECT * FROM users", fp6) assert key1 != key6 - # Cache keys are now tuples for better performance assert isinstance(key1, tuple) assert key1 == _make_cache_key_tuple( "SELECT * FROM users", fp1, processor._input_style, processor._exec_style, processor._dialect_str, False @@ -969,6 +972,99 @@ def test_parameter_cache_statistics(basic_statement_config: "StatementConfig") - assert stats["parameter_size"] >= 1 +def test_compiler_cache_hot_paths_use_bound_cache_flags(basic_statement_config: "StatementConfig") -> None: + """Compiler cache hot paths should not re-check config.enable_caching after initialization.""" + processor = SQLProcessor(basic_statement_config) + + processor.compile("SELECT * FROM users WHERE id = ?", [123]) + processor.compile("SELECT * FROM users WHERE id = ?", [456]) + + assert processor.cache_stats["hits"] == 1 + assert "self._config.enable_caching" not in inspect.getsource(SQLProcessor.compile) + assert "self._config.enable_caching" not in inspect.getsource(SQLProcessor._resolve_expression) + + +def test_processor_binds_parameter_config_hot_path_attrs(basic_statement_config: "StatementConfig") -> None: + """SQLProcessor should bind frequently used parameter config objects once.""" + processor = SQLProcessor(basic_statement_config) + + assert processor._parameter_config is basic_statement_config.parameter_config + assert processor._enable_parameter_type_wrapping is basic_statement_config.enable_parameter_type_wrapping + assert "self._config.parameter_config" not in inspect.getsource(SQLProcessor._prepare_parameters) + assert "self._config.parameter_config" not in inspect.getsource(SQLProcessor._finalize_compilation) + + +def test_core_idiom_sweep_source_shapes() -> None: + """Core helpers should use mypyc-friendly staticmethods and Final constants.""" + static_methods = { + SQL: ("_normalize_dialect", "_should_auto_detect_many", "_extract_filters"), + SQLProcessor: ("_validate_parameters",), + StatementPipelineRegistry: ("_fingerprint_config",), + } + for cls, names in static_methods.items(): + for name in names: + assert isinstance(cls.__dict__.get(name), staticmethod) + + expected_source_fragments = { + "sqlspec/core/compiler.py": ( + "OPERATION_TYPE_MAP: Final[dict[type[exp.Expr], OperationType]]", + "COPY_OPERATION_TYPES: Final[tuple[OperationType, ...]]", + "COPY_FROM_OPERATION_TYPES: Final[tuple[OperationType, ...]]", + "COPY_TO_OPERATION_TYPES: Final[tuple[OperationType, ...]]", + ), + "sqlspec/core/statement.py": ( + "RETURNS_ROWS_OPERATIONS: Final[frozenset[str]] = frozenset({", + "MODIFYING_OPERATIONS: Final[frozenset[str]] = frozenset({", + ), + "sqlspec/core/query_modifiers.py": ( + "_TABLE_QUALIFIED_PARTS: Final[int] = 2", + "_DATABASE_QUALIFIED_PARTS: Final[int] = 3", + "_CATALOG_QUALIFIED_PARTS: Final[int] = 4", + ), + "sqlspec/core/cache.py": ("NAMESPACED_CACHE_CONFIG: Final[dict[str, tuple[",), + "sqlspec/core/parameters/_alignment.py": ("EXECUTE_MANY_MIN_ROWS: Final[int] = 2",), + "sqlspec/core/parameters/_declared.py": ("_JSON_VALUE_TYPES: Final[tuple[type, ...]]",), + "sqlspec/core/parameters/_processor.py": ( + "_EXECUTE_MANY_SAMPLE_THRESHOLD: Final[int] = 10", + "_EXECUTE_MANY_SAMPLE_SIZE: Final[int] = 3", + "_OCCURRENCE_BASED_POSITIONAL_STYLES: Final[frozenset[ParameterStyle]]", + ), + "sqlspec/core/parameters/_registry.py": ( + "_DEFAULT_JSON_SERIALIZER: Final[Callable[[Any], str]]", + "_DEFAULT_JSON_DESERIALIZER: Final[Callable[[str], Any]]", + "DRIVER_PARAMETER_PROFILES: Final[dict[str, DriverParameterProfile]]", + ), + "sqlspec/core/parameters/_types.py": ( + "TYPED_PARAMETER_SLOTS: Final", + "PARAMETER_INFO_SLOTS: Final", + "PARAMETER_STYLE_CONFIG_SLOTS: Final", + "_NAMED_STYLES: Final", + "_POSITIONAL_STYLES: Final", + ), + } + for path, fragments in expected_source_fragments.items(): + source = Path(path).read_text() + for fragment in fragments: + assert fragment in source + + stripped_comments = ( + "Optimization: Check only the first element", + "O(1) check instead of O(N) scan", + "through the interpreted serializer-selection shell", + "For expressions, we use a hash", + "Skip sort for single style", + ) + for path in ( + "sqlspec/core/statement.py", + "sqlspec/core/type_converter.py", + "sqlspec/core/filters.py", + "sqlspec/core/parameters/_types.py", + ): + source = Path(path).read_text() + for comment in stripped_comments: + assert comment not in source + + def test_cache_clear(basic_statement_config: "StatementConfig", sample_sql_queries: "dict[str, str]") -> None: """Test cache clearing functionality.""" processor = SQLProcessor(basic_statement_config) @@ -1029,11 +1125,13 @@ def test_processor_memory_efficiency_with_slots() -> None: "_cache_misses", "_config", "_dialect_str", + "_enable_parameter_type_wrapping", "_exec_style", "_input_style", "_last_cache_key", "_last_result", "_max_cache_size", + "_parameter_config", "_parameter_processor", "_parse_cache", "_parse_cache_hits", diff --git a/tests/unit/core/test_explain.py b/tests/unit/core/test_explain.py new file mode 100644 index 000000000..714dc2801 --- /dev/null +++ b/tests/unit/core/test_explain.py @@ -0,0 +1,27 @@ +"""Unit tests for EXPLAIN option value objects.""" + +from pathlib import Path + +from sqlspec.core.explain import ExplainFormat, ExplainOptions + + +def test_explain_options_value_semantics() -> None: + options = ExplainOptions(analyze=True, verbose=True, format=ExplainFormat.JSON, costs=False, buffers=True) + same = ExplainOptions(analyze=True, verbose=True, format="json", costs=False, buffers=True) + different = ExplainOptions(analyze=False, verbose=True, format=ExplainFormat.JSON, costs=False, buffers=True) + + assert options == same + assert options != different + assert hash(options) == hash(same) + assert repr(options) == "ExplainOptions(analyze=True, verbose=True, format='json', costs=False, buffers=True)" + assert options.to_dict() == {"analyze": True, "verbose": True, "format": "JSON", "costs": False, "buffers": True} + + +def test_explain_options_source_uses_shared_key_and_fields() -> None: + source = Path("sqlspec/core/explain.py").read_text() + + assert "EXPLAIN_OPTION_FIELDS: Final" in source + assert "def _key(self)" in source + assert "return self._key() == other._key()" in source + assert "return hash(self._key())" in source + assert "for field_name in EXPLAIN_OPTION_FIELDS" in source diff --git a/tests/unit/core/test_filters.py b/tests/unit/core/test_filters.py index 1e8bc6859..d5546220d 100644 --- a/tests/unit/core/test_filters.py +++ b/tests/unit/core/test_filters.py @@ -4,9 +4,11 @@ ORDER BY, LIMIT/OFFSET, and other SQL modifications with proper parameter naming. """ +import inspect import tempfile from dataclasses import dataclass from datetime import datetime +from pathlib import Path from typing import TYPE_CHECKING, Any, cast import pytest @@ -17,6 +19,7 @@ from sqlspec import sql as sql_builder from sqlspec.adapters.aiosqlite import AiosqliteConfig from sqlspec.base import SQLSpec +from sqlspec.builder import Select from sqlspec.core import ( SQL, AnyCollectionFilter, @@ -36,10 +39,11 @@ apply_filter, canonicalize_filters, ) -from sqlspec.core.filters import NotInSearchFilter +from sqlspec.core.filters import NotInSearchFilter, OnBeforeAfterFilter, PaginationFilter from sqlspec.driver import CommonDriverAttributesMixin from sqlspec.driver._async import AsyncDriverAdapterBase from sqlspec.driver._sync import SyncDriverAdapterBase +from sqlspec.exceptions import NotFoundError from sqlspec.service import SQLSpecAsyncService, SQLSpecSyncService if TYPE_CHECKING: @@ -52,6 +56,42 @@ def test_public_canonicalize_filters_uses_statement_filter_implementation() -> N assert canonicalize_filters.__module__ == "sqlspec.core.filters" +def test_public_filters_alias_points_to_core_filters_module() -> None: + """The top-level filters alias remains a module-level compatibility surface.""" + import sqlspec + + assert sqlspec.filters is filters_module + + +def test_filter_hierarchy_uses_shared_private_bases() -> None: + """Public filter classes keep their API while sharing private implementation bodies.""" + source = Path("sqlspec/core/filters.py").read_text() + assert "class _DatetimeBoundFilter(StatementFilter):" in source + assert "class BeforeAfterFilter(_DatetimeBoundFilter):" in source + assert "class OnBeforeAfterFilter(_DatetimeBoundFilter):" in source + + datetime_section = source.split("class _DatetimeBoundFilter", 1)[1].split("class InAnyFilter", 1)[0] + assert datetime_section.count("def extract_parameters(") == 1 + assert datetime_section.count("def append_to_statement(") == 1 + + assert "class _TextSearchFilter(StatementFilter):" in source + assert "class SearchFilter(_TextSearchFilter):" in source + assert "class NotInSearchFilter(SearchFilter):" in source + + search_section = source.split("class _TextSearchFilter", 1)[1].split("class NullFilter", 1)[0] + assert search_section.count("def extract_parameters(") == 1 + assert search_section.count("def append_to_statement(") == 1 + assert search_section.count("def get_cache_key(") == 1 + + +def test_filters_docs_render_inherited_members() -> None: + """Docs include inherited members for public filters whose methods are inherited.""" + docs = Path("docs/reference/core/filters.rst").read_text() + for class_name in ("BeforeAfterFilter", "OnBeforeAfterFilter", "SearchFilter", "NotInSearchFilter"): + block = docs.split(f".. autoclass:: {class_name}", 1)[1].split(".. autoclass::", 1)[0] + assert ":inherited-members:" in block + + def test_statement_filter_get_column_expression_delegates_to_parse_column_for_condition( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -187,8 +227,6 @@ def test_limit_offset_filter_uses_descriptive_parameters() -> None: def test_limit_offset_filter_inherits_pagination_filter_base() -> None: """LimitOffsetFilter inherits from the public PaginationFilter base (downstream API).""" - from sqlspec.core.filters import PaginationFilter - assert issubclass(LimitOffsetFilter, PaginationFilter) assert issubclass(PaginationFilter, StatementFilter) assert LimitOffsetFilter.__mro__[1] is PaginationFilter @@ -289,6 +327,14 @@ def test_filter_parameter_conflict_resolution() -> None: assert result.parameters[new_param_keys[0]] == "%new_value%" +def test_resolve_parameter_conflicts_binds_statement_parameters_once() -> None: + """Conflict resolution should avoid duplicate parameter property reads.""" + source = inspect.getsource(StatementFilter._resolve_parameter_conflicts) + + assert source.count("statement.parameters") == 1 + assert "existing_params.update" not in source + + def test_multiple_filters_preserve_column_names() -> None: """Test that multiple filters maintain column-based parameter naming and merge properly.""" sql_stmt = SQL("SELECT * FROM users") @@ -685,8 +731,6 @@ def test_before_after_filter_col_nodes_are_independent() -> None: def test_on_before_after_filter_col_nodes_are_independent() -> None: - from sqlspec.core.filters import OnBeforeAfterFilter - filter_obj = OnBeforeAfterFilter( "created_at", on_or_before=datetime(2023, 12, 31), on_or_after=datetime(2023, 1, 1) ) @@ -1042,8 +1086,6 @@ def test_multiple_filters_sequential_compound_where() -> None: def test_query_builder_apply_filters_in_collection() -> None: """QueryBuilder.apply_filters applies InCollectionFilter correctly (issue #405).""" - from sqlspec.builder import Select - builder = Select("*").from_("users") f = InCollectionFilter("status", ["active", "pending"]) @@ -1058,8 +1100,6 @@ def test_query_builder_apply_filters_in_collection() -> None: def test_query_builder_apply_filters_multiple() -> None: """QueryBuilder.apply_filters handles multiple filters (issue #405).""" - from sqlspec.builder import Select - builder = Select("*").from_("users") f1 = InCollectionFilter("status", ["active"]) f2 = SearchFilter("name", "john") @@ -1078,8 +1118,6 @@ def test_query_builder_apply_filters_multiple() -> None: def test_query_builder_apply_filters_not_in_collection() -> None: """QueryBuilder.apply_filters applies NotInCollectionFilter correctly (issue #405).""" - from sqlspec.builder import Select - builder = Select("*").from_("users") f = NotInCollectionFilter("status", ["deleted", "archived"]) @@ -1095,8 +1133,6 @@ def test_query_builder_apply_filters_not_in_collection() -> None: def test_query_builder_apply_filters_empty() -> None: """QueryBuilder.apply_filters with no filters returns unmodified SQL.""" - from sqlspec.builder import Select - builder = Select("*").from_("users") result = builder.apply_filters() @@ -1117,8 +1153,6 @@ def test_search_filter_with_qualified_name_uses_sanitized_parameters() -> None: def test_search_filter_with_qualified_name_appends_to_statement_correctly() -> None: """Test that SearchFilter with a dotted name appends to statement with qualified column.""" - from sqlspec.core import SQL - statement = SQL("SELECT * FROM users u JOIN profiles p ON u.id = p.user_id") filter_obj = SearchFilter("u.name", "john") @@ -1144,8 +1178,6 @@ def test_in_collection_filter_with_qualified_name_uses_sanitized_parameters() -> def test_order_by_filter_with_qualified_name_appends_to_statement_correctly() -> None: """Test that OrderByFilter with a dotted name appends to statement correctly.""" - from sqlspec.core import SQL - statement = SQL("SELECT * FROM users u JOIN profiles p ON u.id = p.user_id") filter_obj = OrderByFilter("u.created_at", "desc") @@ -1157,8 +1189,6 @@ def test_order_by_filter_with_qualified_name_appends_to_statement_correctly() -> def test_order_by_filter_with_expression_appends_to_statement_correctly() -> None: """Test that OrderByFilter with a SQLGlot expression appends to statement correctly.""" - from sqlspec.core import SQL - statement = SQL("SELECT id, lines, occurrences FROM stats") coalesce_expr = exp.Coalesce( this=exp.column("lines"), expressions=[exp.column("occurrences"), exp.Literal.number(0)] @@ -1173,8 +1203,6 @@ def test_order_by_filter_with_expression_appends_to_statement_correctly() -> Non def test_search_filter_with_expression_in_set_appends_to_statement_correctly() -> None: """Test that SearchFilter with a SQLGlot expression in a set appends to statement correctly.""" - from sqlspec.core import SQL - statement = SQL("SELECT name, email FROM users") upper_name = exp.Upper(this=exp.column("name")) fields: set[str | exp.Expression] = {upper_name, "email"} @@ -1202,8 +1230,6 @@ def test_query_builder_apply_filters_produces_valid_sql_for_execution() -> None: This verifies the end-to-end path: QueryBuilder -> apply_filters -> SQL with parameters. """ - from sqlspec.builder import Select - builder = Select("id", "name", "status").from_("users") f1 = InCollectionFilter("status", ["active", "pending"]) f2 = OrderByFilter("name", "asc") @@ -1465,8 +1491,6 @@ async def test_service_exists_works() -> None: @pytest.mark.anyio async def test_service_get_one_returns_row_or_raises() -> None: """get_one returns the row when present and raises NotFoundError otherwise.""" - from sqlspec.exceptions import NotFoundError - with tempfile.NamedTemporaryFile(suffix=".db", delete=True) as tmp: sqlspec = SQLSpec() config = AiosqliteConfig(connection_config={"database": tmp.name}) diff --git a/tests/unit/core/test_hashing.py b/tests/unit/core/test_hashing.py index 58201c6d2..b93f0537d 100644 --- a/tests/unit/core/test_hashing.py +++ b/tests/unit/core/test_hashing.py @@ -5,6 +5,7 @@ Covers all hashing functions with edge cases, performance considerations, and circular reference handling. """ +import inspect import math from typing import Any from unittest.mock import Mock @@ -183,6 +184,20 @@ def test_hash_parameters_with_typed_parameters() -> None: assert isinstance(result, int) +def test_hash_parameters_named_typed_parameters_use_direct_isinstance() -> None: + """Named TypedParameter hashing should avoid the generic type guard helper.""" + typed_param = TypedParameter([1, 2, 3], list, "items") + + assert hash_parameters(named_parameters={"items": typed_param}) == hash_parameters( + named_parameters={"items": typed_param} + ) + + source = inspect.getsource(hash_parameters) + named_loop = source.split("if named_parameters:", 1)[1].split("if original_parameters", 1)[0] + assert "isinstance(value, TypedParameter)" in named_loop + assert "is_typed_parameter(value)" not in named_loop + + def test_hash_parameters_unhashable_types() -> None: """Test hash_parameters handles unhashable types correctly.""" params = [{"unhashable": "dict"}, ["unhashable", "list"]] diff --git a/tests/unit/core/test_parameters.py b/tests/unit/core/test_parameters.py index a0a1195dc..3cf19b7f5 100644 --- a/tests/unit/core/test_parameters.py +++ b/tests/unit/core/test_parameters.py @@ -11,6 +11,7 @@ import json import math +import re import warnings from collections.abc import Callable, Sequence from datetime import date, datetime, time @@ -43,12 +44,29 @@ replace_placeholders_with_literals, wrap_with_type, ) +from sqlspec.core.parameters import _alignment as _alignment_module from sqlspec.core.parameters import _converter as _converter_module from sqlspec.core.parameters import _processor as _processor_module +from sqlspec.core.parameters import _types from sqlspec.core.parameters import _validator as _validator_module from sqlspec.exceptions import ImproperConfigurationError, SQLSpecError from sqlspec.utils.serializers import from_json, to_json +try: + from sqlspec.adapters.asyncpg.core import driver_profile as asyncpg_driver_profile +except ImportError: + asyncpg_driver_profile = None + +try: + from sqlspec.adapters.psycopg.core import driver_profile as psycopg_driver_profile +except ImportError: + psycopg_driver_profile = None + +try: + from sqlspec.adapters.pymysql.core import driver_profile as pymysql_driver_profile +except ImportError: + pymysql_driver_profile = None + _VALIDATOR_COMPILED = (_validator_module.__file__ or "").endswith((".so", ".pyd")) _CONVERTER_COMPILED = (_converter_module.__file__ or "").endswith((".so", ".pyd")) _ADAPTER_DRIVER_MODULES: "tuple[str, ...]" = ( @@ -1121,6 +1139,28 @@ def test_process_execute_many_named_to_positional(processor: "ParameterProcessor assert [tuple(param_set) for param_set in final_params] == [(10, 20), (30, 40)] +def test_validate_parameter_alignment_reuses_execute_many_expected_identifiers(monkeypatch: pytest.MonkeyPatch) -> None: + """execute_many alignment should compute placeholder identifiers once per batch.""" + profile = ParameterProfile([ + ParameterInfo("a", ParameterStyle.NAMED_COLON, 0, 0, ":a"), + ParameterInfo("b", ParameterStyle.NAMED_COLON, 4, 1, ":b"), + ]) + rows = [{"a": index, "b": index + 1} for index in range(8)] + original_collect = _alignment_module._collect_expected_identifiers + calls = 0 + + def counting_collect(parameter_profile: ParameterProfile) -> set[tuple[str, int | str]]: + nonlocal calls + calls += 1 + return original_collect(parameter_profile) + + monkeypatch.setattr(_alignment_module, "_collect_expected_identifiers", counting_collect) + + _alignment_module.validate_parameter_alignment(profile, rows, is_many=True) + + assert calls == 1 + + def test_process_execute_many_skips_coercion_allocations_when_no_types_match(processor: "ParameterProcessor") -> None: """Execute_many should preserve payload identity when coercion map is irrelevant.""" config = ParameterStyleConfig( @@ -1380,8 +1420,6 @@ def test_isinstance_type_wrapping() -> None: def test_parameter_style_constants_are_module_frozensets() -> None: """Style membership constants are hoisted for compiled hot paths.""" - from sqlspec.core.parameters import _types - named_styles = getattr(_types, "_NAMED_STYLES", None) positional_styles = getattr(_types, "_POSITIONAL_STYLES", None) named_style_values = getattr(_types, "_NAMED_STYLE_VALUES", None) @@ -1631,8 +1669,6 @@ def test_duplicate_parameters_mixed_with_unique(converter: ParameterConverter) - assert 2 in converted_params assert 3 in converted_params expected_positions = ["$1", "$2", "$1", "$3", "$2"] - import re - placeholders_in_sql = [match.group() for match in re.finditer("\\$\\d+", converted_sql)] assert placeholders_in_sql == expected_positions @@ -2168,17 +2204,14 @@ def test_end_to_end_parameter_normalization_supported_style_preserved_in_sqlglot @pytest.fixture def async_pg_specific_behavior_asyncpg_config() -> ParameterStyleConfig | None: """Get AsyncPG config if available.""" - try: - from sqlspec.adapters.asyncpg.core import driver_profile - - return ParameterStyleConfig( - default_parameter_style=driver_profile.default_style, - supported_parameter_styles=driver_profile.supported_styles, - default_execution_parameter_style=driver_profile.default_execution_style, - supported_execution_parameter_styles=driver_profile.supported_execution_styles, - ) - except ImportError: + if asyncpg_driver_profile is None: return None + return ParameterStyleConfig( + default_parameter_style=asyncpg_driver_profile.default_style, + supported_parameter_styles=asyncpg_driver_profile.supported_styles, + default_execution_parameter_style=asyncpg_driver_profile.default_execution_style, + supported_execution_parameter_styles=asyncpg_driver_profile.supported_execution_styles, + ) @pytest.fixture @@ -2207,17 +2240,14 @@ def test_async_pg_specific_behavior_asyncpg_pyformat_converts_for_sqlglot( @pytest.fixture def psycopg_specific_behavior_psycopg_config() -> ParameterStyleConfig | None: """Get Psycopg config if available.""" - try: - from sqlspec.adapters.psycopg.core import driver_profile - - return ParameterStyleConfig( - default_parameter_style=driver_profile.default_style, - supported_parameter_styles=driver_profile.supported_styles, - default_execution_parameter_style=driver_profile.default_execution_style, - supported_execution_parameter_styles=driver_profile.supported_execution_styles, - ) - except ImportError: + if psycopg_driver_profile is None: return None + return ParameterStyleConfig( + default_parameter_style=psycopg_driver_profile.default_style, + supported_parameter_styles=psycopg_driver_profile.supported_styles, + default_execution_parameter_style=psycopg_driver_profile.default_execution_style, + supported_execution_parameter_styles=psycopg_driver_profile.supported_execution_styles, + ) @pytest.fixture @@ -2261,17 +2291,14 @@ def test_psycopg_specific_behavior_psycopg_positional_pyformat_preserves_distinc @pytest.fixture def my_sql_adapters_behavior_pymysql_config() -> ParameterStyleConfig | None: """Get PyMySQL config if available.""" - try: - from sqlspec.adapters.pymysql.core import driver_profile - - return ParameterStyleConfig( - default_parameter_style=driver_profile.default_style, - supported_parameter_styles=driver_profile.supported_styles, - default_execution_parameter_style=driver_profile.default_execution_style, - supported_execution_parameter_styles=driver_profile.supported_execution_styles, - ) - except ImportError: + if pymysql_driver_profile is None: return None + return ParameterStyleConfig( + default_parameter_style=pymysql_driver_profile.default_style, + supported_parameter_styles=pymysql_driver_profile.supported_styles, + default_execution_parameter_style=pymysql_driver_profile.default_execution_style, + supported_execution_parameter_styles=pymysql_driver_profile.supported_execution_styles, + ) @pytest.fixture @@ -2465,7 +2492,72 @@ def test_skip_groups_constant_is_module_level() -> None: assert "skip_groups =" not in source +def test_converter_hot_path_uses_shared_style_sets() -> None: + """Parameter converter should avoid per-call cache and style-set allocations.""" + source = Path("sqlspec/core/parameters/_converter.py").read_text() + assert "placeholder_text_len_cache" not in source + assert "_OCCURRENCE_KEYED_STYLES:" in source + assert "_POSITIONAL_STYLES" in source + + def test_skip_groups_constant_used_by_both_paths() -> None: - """Cached and uncached parameter extraction should share the same skip groups.""" + """Parameter extraction should keep skip groups hoisted out of the hot loop.""" source = Path("sqlspec/core/parameters/_validator.py").read_text() - assert source.count("_SKIP_GROUPS") == 3 + assert source.count("_SKIP_GROUPS") == 2 + + +def test_parameter_transformer_validator_source_shapes() -> None: + """Parameter helpers should avoid repeated hot-path type checks.""" + transformer_source = Path("sqlspec/core/parameters/_transformers.py").read_text() + null_transformer_source = transformer_source.split("class _NullPlaceholderTransformer:", 1)[1].split( + "@mypyc_attr", 1 + )[0] + literal_transformer_source = transformer_source.split("class _PlaceholderLiteralTransformer:", 1)[1].split( + "def build_null_pruning_transform", 1 + )[0] + + assert "_MISSING_PARAMETER: Final" in transformer_source + assert null_transformer_source.count("isinstance(node, _exp.Placeholder)") == 1 + assert '"_is_mapping", "_is_sequence"' in literal_transformer_source + assert "self._is_mapping = isinstance(parameters, Mapping)" in literal_transformer_source + assert "self._is_sequence = isinstance(parameters, Sequence)" in literal_transformer_source + assert "isinstance(self._parameters, Mapping)" not in literal_transformer_source + + validator_source = Path("sqlspec/core/parameters/_validator.py").read_text() + assert isinstance(ParameterValidator.__dict__.get("_extract_parameter_style"), staticmethod) + assert "any(match.group(*_SKIP_GROUPS))" in validator_source + assert "any(match.group(group) for group in _SKIP_GROUPS)" not in validator_source + + +def test_parameter_internal_consolidation_source_shapes() -> None: + """Parameter helpers should share internal conversion/extraction bodies.""" + validator_source = Path("sqlspec/core/parameters/_validator.py").read_text() + extract_body = validator_source.split("def extract_parameters", 1)[1].split("def _extract_parameters_uncached", 1)[ + 0 + ] + assert "parameters = self._extract_parameters_uncached(sql)" in extract_body + assert "for match in PARAMETER_REGEX.finditer(sql)" not in extract_body + + processor_source = Path("sqlspec/core/parameters/_processor.py").read_text() + payload_body = processor_source.split("def _coerce_parameters_payload", 1)[1].split("def _make_cache_key_tuple", 1)[ + 0 + ] + assert "_coerce_sequence_preserving_identity(seq_params" in payload_body + assert "_coerce_mapping_preserving_identity(dict_params" in payload_body + assert "updated_seq:" not in payload_body + assert "updated_mapping:" not in payload_body + + transformer_source = Path("sqlspec/core/parameters/_transformers.py").read_text() + null_pruning_body = transformer_source.split("def replace_null_parameters_with_literals", 1)[1].split( + "def _create_literal_expression", 1 + )[0] + assert "def _as_concrete_payload(" in transformer_source + assert "_as_concrete_payload(parameters)" in null_pruning_body + assert "list(parameters) if isinstance(parameters, list)" not in null_pruning_body + + +def test_private_zero_ref_helpers_are_folded() -> None: + """API-invisible private helpers with no references should not linger.""" + assert "def _get_parameter_value(" not in Path("sqlspec/core/parameters/_converter.py").read_text() + assert "def _hash_filter_value(" not in Path("sqlspec/core/hashing.py").read_text() + assert "def _reset_noop(" not in Path("sqlspec/core/_pool.py").read_text() diff --git a/tests/unit/core/test_pipeline.py b/tests/unit/core/test_pipeline.py index 6be68826a..2dd1adef8 100644 --- a/tests/unit/core/test_pipeline.py +++ b/tests/unit/core/test_pipeline.py @@ -1,6 +1,7 @@ # pyright: reportPrivateUsage = false """Unit tests for the shared statement pipeline registry.""" +from pathlib import Path from unittest.mock import patch import sqlspec.core.pipeline as pipeline_module @@ -14,6 +15,17 @@ def test_record_pipeline_metrics_constant_is_bool() -> None: assert isinstance(getattr(pipeline_module, "_RECORD_PIPELINE_METRICS", None), bool) +def test_pipeline_metrics_use_keyed_store_source_shape() -> None: + source = Path("sqlspec/core/pipeline.py").read_text() + metrics_section = source.split("class _PipelineMetrics", 1)[1].split("@mypyc_attr", 1)[0] + + assert "_METRIC_KEYS: Final[tuple[str, ...]]" in source + assert '__slots__ = ("_values",)' in metrics_section + assert "self._values = dict.fromkeys(_METRIC_KEYS, 0)" in metrics_section + assert "return self._values.copy()" in metrics_section + assert "entry.update(metrics)" in source + + def test_os_getenv_not_called_during_compile() -> None: registry = StatementPipelineRegistry() config = StatementConfig() @@ -48,6 +60,7 @@ def test_record_pipeline_metrics_patch_controls_metrics_output() -> None: def test_fingerprint_cache_uses_cached_slot_value() -> None: registry = StatementPipelineRegistry() config = StatementConfig() + config.freeze() first_fingerprint = registry._fingerprint_config(config) assert first_fingerprint.startswith("pipeline::") @@ -60,6 +73,16 @@ def test_fingerprint_cache_uses_cached_slot_value() -> None: assert second_fingerprint == first_fingerprint +def test_fingerprint_cache_does_not_mutate_unfrozen_config() -> None: + registry = StatementPipelineRegistry() + config = StatementConfig() + + first_fingerprint = registry._fingerprint_config(config) + + assert first_fingerprint.startswith("pipeline::") + assert config._fingerprint_cache is None + + def test_fingerprint_uses_config_hash_plus_unhashed_parameter_discriminators() -> None: """Parameter converter type and parameter-level transformer identity remain discriminators.""" registry = StatementPipelineRegistry() diff --git a/tests/unit/core/test_query_modifiers.py b/tests/unit/core/test_query_modifiers.py index 40a1906c1..ecd037b1a 100644 --- a/tests/unit/core/test_query_modifiers.py +++ b/tests/unit/core/test_query_modifiers.py @@ -6,8 +6,10 @@ """ import pytest +import sqlglot from sqlglot import exp +from sqlspec.core.cache import get_cache from sqlspec.core.query_modifiers import ( apply_column_pruning, apply_limit, @@ -474,8 +476,6 @@ def select_columns(expr: exp.Expr) -> exp.Expr: def test_column_pruning_prune_unused_columns_from_subquery() -> None: """Test that unused columns are removed from subqueries.""" - import sqlglot - sql = "SELECT id, name FROM (SELECT id, name, email, created_at FROM users) AS u" select_expr = sqlglot.parse_one(sql) result = apply_column_pruning(select_expr) @@ -493,8 +493,6 @@ def test_column_pruning_prune_columns_returns_unchanged_for_non_select() -> None def test_column_pruning_prune_columns_with_simple_select() -> None: """Test pruning on simple SELECT (should return unchanged).""" - import sqlglot - sql = "SELECT id, name FROM users" select_expr = sqlglot.parse_one(sql) result = apply_column_pruning(select_expr) @@ -505,8 +503,6 @@ def test_column_pruning_prune_columns_with_simple_select() -> None: def test_column_pruning_prune_columns_with_join() -> None: """Test column pruning with JOIN.""" - import sqlglot - sql = "\n SELECT u.id, u.name\n FROM (SELECT id, name, email FROM users) AS u\n JOIN (SELECT user_id, role FROM user_roles) AS r ON u.id = r.user_id\n " select_expr = sqlglot.parse_one(sql) result = apply_column_pruning(select_expr) @@ -516,10 +512,6 @@ def test_column_pruning_prune_columns_with_join() -> None: def test_column_pruning_prune_columns_with_cache() -> None: """Test that column pruning uses cache correctly.""" - import sqlglot - - from sqlspec.core.cache import get_cache - sql = "SELECT id FROM (SELECT id, name, email FROM users) AS u" select_expr = sqlglot.parse_one(sql) cache_key = "test_prune_cache_key" @@ -534,8 +526,6 @@ def test_column_pruning_prune_columns_with_cache() -> None: def test_column_pruning_prune_columns_with_dialect() -> None: """Test column pruning with specific dialect.""" - import sqlglot - sql = "SELECT id FROM (SELECT id, name FROM users) AS u" select_expr = sqlglot.parse_one(sql) result = apply_column_pruning(select_expr, dialect="postgres") @@ -553,8 +543,6 @@ def test_column_pruning_prune_columns_handles_qualification_failure() -> None: def test_set_operation_support_apply_limit_with_union_all() -> None: """Test applying LIMIT to a UNION ALL expression.""" - import sqlglot - union_expr = sqlglot.parse_one("SELECT id FROM a UNION ALL SELECT id FROM b") assert isinstance(union_expr, exp.SetOperation) result = apply_limit(union_expr, 10) @@ -565,8 +553,6 @@ def test_set_operation_support_apply_limit_with_union_all() -> None: def test_set_operation_support_apply_limit_with_union() -> None: """Test applying LIMIT to a UNION (without ALL) expression.""" - import sqlglot - union_expr = sqlglot.parse_one("SELECT id FROM a UNION SELECT id FROM b") assert isinstance(union_expr, exp.SetOperation) result = apply_limit(union_expr, 5) @@ -576,8 +562,6 @@ def test_set_operation_support_apply_limit_with_union() -> None: def test_set_operation_support_apply_limit_with_except() -> None: """Test applying LIMIT to an EXCEPT expression.""" - import sqlglot - except_expr = sqlglot.parse_one("SELECT id FROM a EXCEPT SELECT id FROM b") assert isinstance(except_expr, exp.SetOperation) result = apply_limit(except_expr, 15) @@ -588,8 +572,6 @@ def test_set_operation_support_apply_limit_with_except() -> None: def test_set_operation_support_apply_limit_with_intersect() -> None: """Test applying LIMIT to an INTERSECT expression.""" - import sqlglot - intersect_expr = sqlglot.parse_one("SELECT id FROM a INTERSECT SELECT id FROM b") assert isinstance(intersect_expr, exp.SetOperation) result = apply_limit(intersect_expr, 3) @@ -600,8 +582,6 @@ def test_set_operation_support_apply_limit_with_intersect() -> None: def test_set_operation_support_apply_offset_with_union_all() -> None: """Test applying OFFSET to a UNION ALL expression.""" - import sqlglot - union_expr = sqlglot.parse_one("SELECT id FROM a UNION ALL SELECT id FROM b") assert isinstance(union_expr, exp.SetOperation) result = apply_offset(union_expr, 5) @@ -612,8 +592,6 @@ def test_set_operation_support_apply_offset_with_union_all() -> None: def test_set_operation_support_apply_offset_with_except() -> None: """Test applying OFFSET to an EXCEPT expression.""" - import sqlglot - except_expr = sqlglot.parse_one("SELECT id FROM a EXCEPT SELECT id FROM b") assert isinstance(except_expr, exp.SetOperation) result = apply_offset(except_expr, 20) @@ -623,8 +601,6 @@ def test_set_operation_support_apply_offset_with_except() -> None: def test_set_operation_support_apply_offset_with_intersect() -> None: """Test applying OFFSET to an INTERSECT expression.""" - import sqlglot - intersect_expr = sqlglot.parse_one("SELECT id FROM a INTERSECT SELECT id FROM b") assert isinstance(intersect_expr, exp.SetOperation) result = apply_offset(intersect_expr, 7) @@ -634,8 +610,6 @@ def test_set_operation_support_apply_offset_with_intersect() -> None: def test_set_operation_support_apply_limit_and_offset_with_union_all() -> None: """Test applying both LIMIT and OFFSET to UNION ALL.""" - import sqlglot - union_expr = sqlglot.parse_one("SELECT id FROM a UNION ALL SELECT id FROM b") result = apply_limit(union_expr, 10) result = apply_offset(result, 20) @@ -647,8 +621,6 @@ def test_set_operation_support_apply_limit_and_offset_with_union_all() -> None: def test_set_operation_support_safe_modify_with_cte_preserves_cte_on_set_operation() -> None: """Test that safe_modify_with_cte preserves CTEs on set operations.""" - import sqlglot - cte_union = sqlglot.parse_one("WITH cte AS (SELECT 1 AS id) SELECT id FROM cte UNION ALL SELECT id FROM b") def add_limit(expr: exp.Expr) -> exp.Expr: @@ -663,8 +635,6 @@ def add_limit(expr: exp.Expr) -> exp.Expr: def test_set_operation_support_safe_modify_with_cte_preserves_cte_on_except() -> None: """Test that safe_modify_with_cte preserves CTEs on EXCEPT operations.""" - import sqlglot - cte_except = sqlglot.parse_one("WITH cte AS (SELECT 1 AS id) SELECT id FROM cte EXCEPT SELECT id FROM b") def add_offset(expr: exp.Expr) -> exp.Expr: diff --git a/tests/unit/core/test_result.py b/tests/unit/core/test_result.py index dcb23549b..1532ea2f8 100644 --- a/tests/unit/core/test_result.py +++ b/tests/unit/core/test_result.py @@ -3,16 +3,27 @@ import importlib.util import inspect from dataclasses import dataclass -from typing import Any, cast +from pathlib import Path +from typing import TYPE_CHECKING, Any, TypedDict, cast from unittest.mock import patch import pytest +import sqlspec.core as core_module import sqlspec.core.result as result_package import sqlspec.core.result._base as result_base from sqlspec.core import SQL, ArrowResult, OperationType, SQLResult, StackResult, create_sql_result +from sqlspec.core.result import build_arrow_result_from_reader from sqlspec.typing import PYARROW_INSTALLED +if TYPE_CHECKING: + import pyarrow as pa +else: + try: + import pyarrow as pa + except ImportError: + pa = None # type: ignore[assignment] + _RESULT_BASE_COMPILED = (result_base.__file__ or "").endswith((".so", ".pyd")) @@ -39,6 +50,12 @@ def test_fast_dml_result_alias_is_not_exported() -> None: assert not hasattr(result_package, alias_name) +def test_dml_result_core_export_is_additive() -> None: + """DMLResult is an additive core export for the existing result class.""" + assert "DMLResult" in core_module.__all__ + assert core_module.DMLResult is result_base.DMLResult + + def test_dml_result_schema_type_does_not_raise() -> None: """DMLResult must initialize its schema-row cache slots so schema_type access does not raise.""" @@ -95,8 +112,6 @@ def sql_result(sample_data: list[dict[str, Any]]) -> SQLResult: @pytest.fixture def sample_arrow_table(): """Create a sample Arrow table for testing.""" - import pyarrow as pa - data: dict[str, Any] = {"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"], "age": [30, 25, 35]} return pa.Table.from_pydict(data) @@ -248,7 +263,6 @@ class User: def test_sql_result_get_data_with_typeddict() -> None: """Test SQLResult.get_data() with TypedDict schema_type.""" - from typing import TypedDict class UserDict(TypedDict): id: int @@ -485,7 +499,6 @@ def test_tuple_format_schema_type_get_data_materializes_tuples_to_dicts() -> Non def test_tuple_format_schema_type_get_data_with_schema_type_from_tuples() -> None: """schema_type should work through the full tuple -> dict -> schema pipeline.""" - from dataclasses import dataclass @dataclass class User: @@ -508,7 +521,6 @@ class User: def test_tuple_format_schema_type_all_with_schema_type_from_tuples() -> None: """all(schema_type=...) should work with tuple-format data.""" - from dataclasses import dataclass @dataclass class User: @@ -528,7 +540,6 @@ class User: def test_tuple_format_schema_type_one_with_schema_type_from_tuples() -> None: """one(schema_type=...) should work with tuple-format data.""" - from dataclasses import dataclass @dataclass class User: @@ -547,7 +558,6 @@ class User: def test_tuple_format_schema_type_one_or_none_with_schema_type_from_tuples() -> None: """one_or_none(schema_type=...) should work with tuple-format data.""" - from dataclasses import dataclass @dataclass class User: @@ -568,7 +578,6 @@ class User: def test_tuple_format_schema_type_get_first_with_schema_type_from_tuples() -> None: """get_first(schema_type=...) should work with tuple-format data.""" - from dataclasses import dataclass @dataclass class User: @@ -589,7 +598,6 @@ class User: def test_tuple_format_schema_type_get_data_with_typeddict_from_tuples() -> None: """TypedDict schema_type should work with tuple-format data.""" - from typing import TypedDict class UserDict(TypedDict): id: int @@ -672,8 +680,6 @@ def test_tuple_format_schema_type_lazy_materialization_caches_result() -> None: @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow not installed") def test_sql_result_to_arrow(sql_result: SQLResult) -> None: """Test converting SQLResult to Arrow Table.""" - import pyarrow as pa - table = sql_result.to_arrow() assert isinstance(table, pa.Table) assert table.num_rows == 3 @@ -683,8 +689,6 @@ def test_sql_result_to_arrow(sql_result: SQLResult) -> None: @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow not installed") def test_sql_result_to_arrow_empty_data() -> None: """Test to_arrow() with empty data list.""" - import pyarrow as pa - stmt = SQL("SELECT * FROM users WHERE 1=0") result = SQLResult(statement=stmt, data=[]) table = result.to_arrow() @@ -850,10 +854,41 @@ def test_arrow_result_iter_in_for_loop(arrow_result: ArrowResult) -> None: assert names == ["Alice", "Bob", "Charlie"] +@pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow not installed") +def test_arrow_stack_external_api_smoke(arrow_result: ArrowResult) -> None: + """Arrow/stack result helpers are external API and remain callable.""" + assert arrow_result.num_columns == 3 + + stack = StackResult.from_arrow_result(arrow_result) + assert stack.is_arrow_result() is True + assert stack.result is arrow_result + assert stack.rows_affected == arrow_result.rows_affected + + +@pytest.mark.skipif(_RESULT_BASE_COMPILED, reason="compiled descriptors do not expose Python docstring metadata") +def test_result_external_api_docstring_markers() -> None: + """Dead-code policy markers identify externally callable result helpers.""" + for member in (ArrowResult.num_columns.fget, StackResult.is_arrow_result, StackResult.from_arrow_result): + assert "External/extension API" in (inspect.getdoc(member) or "") + + +def test_result_and_stack_idiom_source_shapes() -> None: + """Result and stack helpers should keep MyPyC-safe source shapes.""" + result_source = Path("sqlspec/core/result/_base.py").read_text() + assert "return iter(arrow_table_to_pylist" in result_source + assert "yield from arrow_table_to_pylist" not in result_source + assert 'object.__getattribute__(self.result, "rows_affected")' in result_source + assert "_EMPTY_RESULT_STATEMENT: Final" in result_source + assert "_DEFAULT_DML_METADATA: Final" in result_source + + stack_source = Path("sqlspec/core/stack.py").read_text() + assert "ALLOWED_METHODS: Final[tuple[str, ...]]" in stack_source + assert 'StackOperation("execute_many", normalized_statement, tuple(arguments), frozen_kwargs)' not in stack_source + + def test_arrow_result_to_pandas_with_null_values() -> None: """Test to_pandas() correctly handles NULL values.""" pandas = pytest.importorskip("pandas") - import pyarrow as pa data: dict[str, Any] = { "id": [1, 2, 3], @@ -870,8 +905,6 @@ def test_arrow_result_to_pandas_with_null_values() -> None: def test_arrow_result_empty_table() -> None: """Test ArrowResult methods with empty table.""" - import pyarrow as pa - empty_table = pa.Table.from_pydict(cast(dict[str, Any], {})) stmt = SQL("SELECT * FROM users WHERE 1=0") result = ArrowResult(statement=stmt, data=empty_table) @@ -898,10 +931,6 @@ def test_arrow_result_methods_with_none_data_raise() -> None: @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow not installed") def test_build_arrow_result_from_reader_reader_format() -> None: - import pyarrow as pa - - from sqlspec.core.result import build_arrow_result_from_reader - reader = pa.table({"id": pa.array([0, 1, 2])}).to_reader() result = build_arrow_result_from_reader(SQL("select 1"), reader, return_format="reader") @@ -911,10 +940,6 @@ def test_build_arrow_result_from_reader_reader_format() -> None: @pytest.mark.skipif(not PYARROW_INSTALLED, reason="pyarrow not installed") def test_build_arrow_result_from_reader_batches_format() -> None: - import pyarrow as pa - - from sqlspec.core.result import build_arrow_result_from_reader - reader = pa.table({"id": pa.array(range(5))}).to_reader() result = build_arrow_result_from_reader(SQL("select 1"), reader, return_format="batches") diff --git a/tests/unit/core/test_splitter.py b/tests/unit/core/test_splitter.py index a8bed81b2..037d01d02 100644 --- a/tests/unit/core/test_splitter.py +++ b/tests/unit/core/test_splitter.py @@ -1,10 +1,35 @@ """Unit tests for SQL splitter helpers.""" +import inspect +from typing import Any + import pytest import sqlspec.core.splitter as splitter_module from sqlspec.core.splitter import GenericDialectConfig, StatementSplitter, Token, TokenType, split_sql_script +PUBLIC_DIALECT_CONFIGS = ( + splitter_module.OracleDialectConfig, + splitter_module.TSQLDialectConfig, + splitter_module.PostgreSQLDialectConfig, + splitter_module.GenericDialectConfig, + splitter_module.MySQLDialectConfig, + splitter_module.SQLiteDialectConfig, + splitter_module.DuckDBDialectConfig, + splitter_module.BigQueryDialectConfig, +) + +DIALECT_DEFAULTS: dict[type[Any], tuple[str, set[str], set[str], set[str], set[str], set[str]]] = { + splitter_module.OracleDialectConfig: ("oracle", {"BEGIN", "DECLARE", "CASE"}, {"END"}, {";"}, set(), {"/"}), + splitter_module.TSQLDialectConfig: ("tsql", {"BEGIN", "TRY"}, {"END", "CATCH"}, {";"}, {"GO"}, set()), + splitter_module.PostgreSQLDialectConfig: ("postgresql", {"DECLARE", "CASE", "DO"}, {"END"}, {";"}, set(), set()), + splitter_module.GenericDialectConfig: ("generic", {"BEGIN", "DECLARE", "CASE"}, {"END"}, {";"}, set(), set()), + splitter_module.MySQLDialectConfig: ("mysql", {"BEGIN", "DECLARE", "CASE"}, {"END"}, {";"}, set(), {"\\g", "\\G"}), + splitter_module.SQLiteDialectConfig: ("sqlite", {"BEGIN", "CASE"}, {"END"}, {";"}, set(), set()), + splitter_module.DuckDBDialectConfig: ("duckdb", {"BEGIN", "CASE"}, {"END"}, {";"}, set(), set()), + splitter_module.BigQueryDialectConfig: ("bigquery", {"BEGIN", "CASE"}, {"END"}, {";"}, set(), set()), +} + def test_join_string_fragments_helper_removed() -> None: """The one-line join helper should not remain in the splitter module.""" @@ -37,6 +62,70 @@ def test_split_sql_script_preserves_statement_output() -> None: assert split_sql_script("SELECT 1; SELECT 2;", strip_trailing_terminator=True) == ["SELECT 1", "SELECT 2"] +def test_tokenize_returns_materialized_token_list() -> None: + """_tokenize keeps its test-frozen name but returns a list for mypyc hot paths.""" + splitter = StatementSplitter(GenericDialectConfig()) + + tokens = splitter._tokenize("SELECT 1;") + + assert isinstance(tokens, list) + assert [token.value for token in tokens] == ["S", "E", "L", "E", "C", "T", " ", "1", ";"] + + +def test_dialect_configs_share_eager_base_without_lazy_property_boilerplate() -> None: + """Public dialect classes should keep names while sharing the private eager base config.""" + + eager_base = getattr(splitter_module, "_EagerDialectConfig") + for config_class in PUBLIC_DIALECT_CONFIGS: + assert issubclass(config_class, eager_base) + source = inspect.getsource(config_class) + assert "if self._name is None" not in source + assert "if self._block_starters is None" not in source + assert "if self._block_enders is None" not in source + assert "if self._statement_terminators is None" not in source + + +@pytest.mark.parametrize("config_class", PUBLIC_DIALECT_CONFIGS) +def test_eager_dialect_defaults_preserve_public_values(config_class: "type[splitter_module.DialectConfig]") -> None: + expected_name, block_starters, block_enders, statement_terminators, batch_separators, special_terminators = ( + DIALECT_DEFAULTS[config_class] + ) + config = config_class() + + assert config.name == expected_name + assert config.block_starters == block_starters + assert config.block_enders == block_enders + assert config.statement_terminators == statement_terminators + assert config.batch_separators == batch_separators + assert set(config.special_terminators) == special_terminators + assert config.max_nesting_depth == 256 + assert config.block_starters is config.block_starters + + config.block_starters.add("__LOCAL__") + assert "__LOCAL__" not in config_class().block_starters + + +def test_split_sql_script_reuses_splitter_for_dialect_and_strip_key(monkeypatch: pytest.MonkeyPatch) -> None: + """split_sql_script should avoid rebuilding stateless splitter instances for the same dialect and strip flag.""" + splitter_module.clear_splitter_caches() + created: list[tuple[str, bool]] = [] + original_init = StatementSplitter.__init__ + + def counting_init( + self: StatementSplitter, dialect: splitter_module.DialectConfig, strip_trailing_semicolon: bool = False + ) -> None: + created.append((dialect.name, strip_trailing_semicolon)) + original_init(self, dialect, strip_trailing_semicolon) + + monkeypatch.setattr(StatementSplitter, "__init__", counting_init) + + assert split_sql_script("SELECT 1;", dialect="sqlite", strip_trailing_terminator=True) == ["SELECT 1"] + assert split_sql_script("SELECT 2;", dialect="sqlite", strip_trailing_terminator=True) == ["SELECT 2"] + assert split_sql_script("SELECT 3;", dialect="sqlite", strip_trailing_terminator=False) == ["SELECT 3;"] + + assert created == [("sqlite", True), ("sqlite", False)] + + def test_contains_executable_content_accepts_pre_tokenized_statement(monkeypatch: pytest.MonkeyPatch) -> None: splitter = StatementSplitter(GenericDialectConfig()) tokens = [ diff --git a/tests/unit/core/test_sqlcommenter.py b/tests/unit/core/test_sqlcommenter.py index 132caf2b6..ddbc1de2f 100644 --- a/tests/unit/core/test_sqlcommenter.py +++ b/tests/unit/core/test_sqlcommenter.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect from typing import Any from unittest.mock import patch @@ -9,6 +10,7 @@ from sqlglot import exp import sqlspec.core.sqlcommenter as sqlcommenter_module +from sqlspec.core import StatementConfig from sqlspec.core.sqlcommenter import ( SQLCommenterContext, append_comment, @@ -16,8 +18,7 @@ generate_comment, parse_comment, ) - -# ── generate_comment ────────────────────────────────────────────────────── +from sqlspec.utils.correlation import CorrelationContext def test_generate_comment_basic() -> None: @@ -74,7 +75,7 @@ def test_generate_comment_special_characters() -> None: def test_generate_comment_none_values_skipped() -> None: - attrs: dict[str, str | None] = {"key1": "val1", "key2": None} # type: ignore[typeddict-item] + attrs: dict[str, str | None] = {"key1": "val1", "key2": None} result = generate_comment(attrs) assert result == "key1='val1'" @@ -85,9 +86,6 @@ def test_empty_string_value() -> None: assert result == "key=''" -# ── append_comment (AST-level) ──────────────────────────────────────────── - - def test_append_comment_basic() -> None: expr = sqlglot.parse_one("SELECT * FROM users") append_comment(expr, {"db_driver": "asyncpg"}) @@ -111,11 +109,11 @@ def test_append_comment_coexists_with_existing_comments() -> None: def test_append_comment_coexists_with_hints() -> None: + """The optimizer hint should be preserved as a separate node alongside the comment.""" expr = sqlglot.parse_one("SELECT /*+ IndexScan(t) */ * FROM users") append_comment(expr, {"db_driver": "asyncpg"}) sql = expr.sql() assert "db_driver='asyncpg'" in sql - # Hint should be preserved as a separate node assert "INDEXSCAN" in sql.upper() @@ -144,9 +142,6 @@ def test_append_comment_works_on_delete() -> None: assert "db_driver='asyncpg'" in expr.sql() -# ── parse_comment (AST-level) ───────────────────────────────────────────── - - def test_parse_comment_basic() -> None: expr = sqlglot.parse_one("SELECT * FROM users") append_comment(expr, {"db_driver": "asyncpg", "route": "/users"}) @@ -176,6 +171,7 @@ def test_parse_comment_url_decodes() -> None: def test_parse_comment_round_trip() -> None: + """Attributes should be correctly round-tripped after re-parsing the generated SQL.""" original_attrs: dict[str, str] = { "db_driver": "asyncpg", "framework": "litestar", @@ -184,13 +180,13 @@ def test_parse_comment_round_trip() -> None: } expr = sqlglot.parse_one("SELECT * FROM users WHERE id = :id") append_comment(expr, original_attrs) - # Re-parse the generated SQL expr2 = sqlglot.parse_one(expr.sql()) _, parsed_attrs = parse_comment(expr2) assert parsed_attrs == original_attrs def test_parse_comment_separates_sqlcommenter_from_regular() -> None: + """A regular comment should be preserved alongside the sqlcommenter payload.""" expr = sqlglot.parse_one("SELECT * FROM users") expr.add_comments(["regular note"]) append_comment(expr, {"db_driver": "asyncpg"}) @@ -199,7 +195,6 @@ def test_parse_comment_separates_sqlcommenter_from_regular() -> None: _, attrs = parse_comment(expr) assert attrs == {"db_driver": "asyncpg"} - # Regular comment should be preserved assert expr.comments is not None assert any("regular note" in c for c in expr.comments) @@ -214,9 +209,6 @@ def test_traceparent_format_round_trip() -> None: assert parsed["traceparent"] == tp -# ── create_sqlcommenter_statement_transformer ───────────────────────────── - - def test_transformer_appends_static_attrs() -> None: transformer = create_sqlcommenter_statement_transformer( attributes={"db_driver": "asyncpg", "framework": "litestar"} @@ -236,6 +228,21 @@ def test_transformer_empty_attrs_noop() -> None: assert not result_expr.comments +def test_transformer_factory_uses_module_level_callables() -> None: + source = inspect.getsource(create_sqlcommenter_statement_transformer) + assert "def _noop_transformer" not in source + assert "def _static_transformer" not in source + assert "def _dynamic_transformer" not in source + + noop_transformer = create_sqlcommenter_statement_transformer(attributes={}) + static_transformer = create_sqlcommenter_statement_transformer(attributes={"db_driver": "asyncpg"}) + dynamic_transformer = create_sqlcommenter_statement_transformer(enable_context=True) + + assert type(noop_transformer).__name__ == "_NoOpSQLCommenterTransformer" + assert type(static_transformer).__name__ == "_StaticSQLCommenterTransformer" + assert type(dynamic_transformer).__name__ == "_DynamicSQLCommenterTransformer" + + def test_transformer_preserves_params() -> None: params_in = [1, 2, 3] transformer = create_sqlcommenter_statement_transformer(attributes={"key": "val"}) @@ -328,12 +335,7 @@ def test_transformer_coexists_with_hints() -> None: assert "INDEXSCAN" in sql.upper() -# ── Correlation ID integration ──────────────────────────────────────────── - - def test_transformer_includes_correlation_id_when_context_enabled() -> None: - from sqlspec.utils.correlation import CorrelationContext - transformer = create_sqlcommenter_statement_transformer(attributes={"db_driver": "asyncpg"}, enable_context=True) with CorrelationContext.context("abc-123"): expr = sqlglot.parse_one("SELECT 1") @@ -344,8 +346,6 @@ def test_transformer_includes_correlation_id_when_context_enabled() -> None: def test_transformer_no_correlation_id_without_context_enabled() -> None: - from sqlspec.utils.correlation import CorrelationContext - transformer = create_sqlcommenter_statement_transformer(attributes={"db_driver": "asyncpg"}) with CorrelationContext.context("abc-123"): expr = sqlglot.parse_one("SELECT 1") @@ -363,8 +363,6 @@ def test_transformer_no_correlation_id_when_not_set() -> None: def test_transformer_explicit_correlation_id_in_context_overrides() -> None: - from sqlspec.utils.correlation import CorrelationContext - transformer = create_sqlcommenter_statement_transformer(enable_context=True) with CorrelationContext.context("from-middleware"): with SQLCommenterContext.scope({"correlation_id": "explicit-value"}): @@ -375,9 +373,6 @@ def test_transformer_explicit_correlation_id_in_context_overrides() -> None: assert "from-middleware" not in sql -# ── SQLCommenterContext ─────────────────────────────────────────────────── - - def test_context_get_returns_none_by_default() -> None: assert SQLCommenterContext.get() is None @@ -406,31 +401,22 @@ def test_context_scope_restores_previous() -> None: SQLCommenterContext.set(None) -# ── StatementConfig integration ─────────────────────────────────────────── - - def test_statement_config_enable_sqlcommenter() -> None: - from sqlspec.core import StatementConfig - + """Enabling sqlcommenter should add a statement_transformer rather than an output_transformer.""" config = StatementConfig( enable_sqlcommenter=True, sqlcommenter_attributes={"db_driver": "sqlite", "framework": "litestar"} ) - # Should be added as a statement_transformer, not output_transformer assert len(config.statement_transformers) == 1 assert config.output_transformer is None def test_statement_config_auto_sets_db_driver_from_dialect() -> None: - from sqlspec.core import StatementConfig - config = StatementConfig(enable_sqlcommenter=True, dialect="postgres") assert config.sqlcommenter_attributes is not None assert config.sqlcommenter_attributes["db_driver"] == "postgresql" def test_statement_config_db_driver_not_overridden_when_explicit() -> None: - from sqlspec.core import StatementConfig - config = StatementConfig( enable_sqlcommenter=True, dialect="postgres", sqlcommenter_attributes={"db_driver": "custom-pg"} ) @@ -439,14 +425,12 @@ def test_statement_config_db_driver_not_overridden_when_explicit() -> None: def test_statement_config_sqlcommenter_disabled_by_default() -> None: - from sqlspec.core import StatementConfig - config = StatementConfig() assert len(config.statement_transformers) == 0 def test_statement_config_sqlcommenter_appends_to_existing_transformers() -> None: - from sqlspec.core import StatementConfig + """The sqlcommenter transformer should append to and coexist with user-provided transformers.""" def my_transformer(expr: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: return expr, params @@ -456,14 +440,11 @@ def my_transformer(expr: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: enable_sqlcommenter=True, sqlcommenter_attributes={"db_driver": "sqlite"}, ) - # Should have both: user transformer + sqlcommenter assert len(config.statement_transformers) == 2 assert config.statement_transformers[0] is my_transformer def test_statement_config_replace_preserves_sqlcommenter() -> None: - from sqlspec.core import StatementConfig - config = StatementConfig(enable_sqlcommenter=True, sqlcommenter_attributes={"db_driver": "sqlite"}) replaced = config.replace(enable_caching=False) assert len(replaced.statement_transformers) == 1 diff --git a/tests/unit/core/test_statement.py b/tests/unit/core/test_statement.py index ce42941ae..87fda2716 100644 --- a/tests/unit/core/test_statement.py +++ b/tests/unit/core/test_statement.py @@ -15,8 +15,10 @@ """ import copy +import importlib.util import logging import pickle +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -24,6 +26,7 @@ from sqlglot import expressions as exp from sqlglot.dialects.postgres import Postgres +import sqlspec.core.pipeline as pipeline_module import sqlspec.typing as public_typing from sqlspec.core import ( SQL, @@ -41,9 +44,11 @@ get_pipeline_metrics, reset_pipeline_registry, ) +from sqlspec.core._pool import get_processed_state_pool from sqlspec.core.filters import LimitOffsetFilter from sqlspec.core.hashing import hash_filters from sqlspec.core.metrics import StackExecutionMetrics +from sqlspec.core.parameters import ParameterProcessor from sqlspec.core.parameters._processor import structural_fingerprint, value_fingerprint from sqlspec.core.pipeline import reset_statement_pipeline_cache from sqlspec.core.result._base import SQLResult, StackResult @@ -56,6 +61,7 @@ split_sql_script, ) from sqlspec.core.stack import StackOperation, StatementStack +from sqlspec.core.statement import _parse_order_item from sqlspec.data_dictionary import ColumnMetadata, ForeignKeyMetadata, IndexMetadata, TableMetadata, VersionInfo from sqlspec.typing import Empty from tests.conftest import requires_interpreted @@ -116,8 +122,6 @@ def test_rebind_processor_is_reused_across_cache_rebinds() -> None: def test_rebind_processor_is_cleared_on_reset() -> None: """reset() clears the cached processor because reset also replaces the config.""" - from sqlspec.core.parameters import ParameterProcessor - statement = SQL("SELECT :id", id=1) statement._rebind_processor = ParameterProcessor(cache_max_size=0, validator_cache_max_size=0) statement.reset() @@ -126,8 +130,6 @@ def test_rebind_processor_is_cleared_on_reset() -> None: def test_parse_order_item_is_module_level_and_order_by_still_compiles() -> None: """ORDER BY string parsing should work through the extracted helper.""" - from sqlspec.core.statement import _parse_order_item - order_expr = _parse_order_item("name DESC", None, True) assert isinstance(order_expr, exp.Ordered) statement = SQL("SELECT id, name FROM users").order_by("name", "id DESC") @@ -271,6 +273,18 @@ def test_sql_where_preserves_generated_parameter_counters() -> None: assert filtered._sql_param_counters == stmt._sql_param_counters +def test_statement_where_helpers_are_consolidated() -> None: + source = Path("sqlspec/core/statement.py").read_text() + clone_section = source.split("def _create_modified_copy_with_expression", 1)[1].split("def where(", 1)[0] + assert "def _clone_base(" in clone_section + assert clone_section.count("statement_config=self._statement_config") == 1 + assert "def _where_condition(" in source + assert "def _where_comparison(" in source + assert "def _where_sequence_membership(" in source + assert source.count("new_sql._named_parameters[param_name] =") == 1 + assert source.count("safe_modify_with_cte(expression, lambda e: apply_where(e, condition))") <= 2 + + def test_sql_order_by_preserves_generated_parameter_counters() -> None: stmt = SQL("SELECT * FROM users").where_eq("id", 1) ordered = stmt.order_by("name") @@ -279,8 +293,6 @@ def test_sql_order_by_preserves_generated_parameter_counters() -> None: def test_processed_state_pool_resets_on_release() -> None: """ProcessedState pool should reset state before reuse.""" - from sqlspec.core._pool import get_processed_state_pool - pool = get_processed_state_pool() state = pool.acquire() state.compiled_sql = "SELECT 1" @@ -1355,8 +1367,6 @@ def test_processed_state_parameter_profile_exposed() -> None: def test_shared_pipeline_metrics_respects_debug_flag() -> None: """Shared pipeline metrics emit data only when debug flag is enabled.""" - import sqlspec.core.pipeline as pipeline_module - with patch.object(pipeline_module, "_RECORD_PIPELINE_METRICS", True): reset_pipeline_registry() SQL("SELECT 1").compile() @@ -1569,8 +1579,6 @@ def test_native_layout_wave3_internal_splitter_dialects_remain_instantiable_afte def test_native_layout_wave3_result_io_module_is_inlined() -> None: """The result conversion helper module should no longer exist as a call boundary.""" - import importlib.util - assert importlib.util.find_spec("sqlspec.core.result._io") is None diff --git a/tests/unit/core/test_type_conversion.py b/tests/unit/core/test_type_conversion.py index 2c9405aec..4046939e2 100644 --- a/tests/unit/core/test_type_conversion.py +++ b/tests/unit/core/test_type_conversion.py @@ -4,6 +4,7 @@ ensuring consistent type handling across all database adapters. """ +import inspect from datetime import date, datetime, time, timezone from decimal import Decimal from typing import cast @@ -164,6 +165,27 @@ def test_mac_address_detection(detector: "BaseTypeConverter") -> None: assert detected == "mac" +def test_detect_type_uses_lastgroup_fast_path(detector: "BaseTypeConverter") -> None: + """Detection should preserve outputs without allocating a groupdict per match.""" + samples = { + "uuid": "123e4567-e89b-12d3-a456-426614174000", + "iso_datetime": "2023-12-25T10:30:00Z", + "iso_date": "2023-12-25", + "iso_time": "10:30:00", + "json": '{"key": "value"}', + "ipv4": "192.168.1.1", + "ipv6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "mac": "00:1B:44:11:3A:B7", + } + + for expected_type, value in samples.items(): + assert detector.detect_type(value) == expected_type + + source = inspect.getsource(BaseTypeConverter.detect_type) + assert ".lastgroup" in source + assert "groupdict()" not in source + + def test_non_special_type(detector: "BaseTypeConverter") -> None: """Test that regular strings are not detected as special types.""" regular_strings = ["hello world", "123", "not-a-uuid", "invalid-date"] diff --git a/uv.lock b/uv.lock index 03718ec41..6fecb9fa3 100644 --- a/uv.lock +++ b/uv.lock @@ -748,11 +748,11 @@ wheels = [ [[package]] name = "bracex" -version = "2.6" +version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7c/a2a8a52db0ee751007507ddad3a1ddf1b0f763de546c588e7a828579bdad/bracex-2.7.tar.gz", hash = "sha256:4cb5d415a707f6beeb2779099486090bf98cbd8b7edbdfcb7cbea2f5fe6bdb48", size = 42150, upload-time = "2026-06-28T18:48:39.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/ba/24/67865d7a710d86de496c7984e06023aa3656b5fae16ee229a530b57c0491/bracex-2.7-py3-none-any.whl", hash = "sha256:025043774188f8a05db36de9e3d4f7d82a8509a41a115cc134c44a60c36375eb", size = 11508, upload-time = "2026-06-28T18:48:38.138Z" }, ] [[package]] @@ -1008,7 +1008,7 @@ wheels = [ [[package]] name = "click-extra" -version = "8.1.1" +version = "8.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boltons" }, @@ -1021,9 +1021,9 @@ dependencies = [ { name = "wcmatch" }, { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/ee/61eb8cadbf58b3570bab603106076e46466e890f7aba19ac4a420b27511f/click_extra-8.1.1.tar.gz", hash = "sha256:d9fa8de46b5fac91c7cbb0be5e1a788f04845d01a5580f210f90761f16be7cd6", size = 306766, upload-time = "2026-06-24T15:35:08.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/96/bb6ab6d01a5e3ed728f627988e931dc2179c0ef76ac0eab06add0ce2f015/click_extra-8.1.4.tar.gz", hash = "sha256:342e8194749278e7ce78c5f28f0c2fcfa935534150dc8a4d8021229870768449", size = 307295, upload-time = "2026-06-27T11:55:12.792Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/46/406186bcf0963d0fcd9e89c0e00f4dcb761ad5b1ec91fbca7fd4f1909677/click_extra-8.1.1-py3-none-any.whl", hash = "sha256:73b51c2247177a2830573420d90f9bdc07dbcd2d226e34d098a7f4cd53972180", size = 333109, upload-time = "2026-06-24T15:35:07.107Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b6/12c8392b291b4564244e85c655cc701611d3e7d761a4cc3ce3dade8172a7/click_extra-8.1.4-py3-none-any.whl", hash = "sha256:8833567398a3ed49b5c60b5876a3afa357f883c51ff3fb1543578dca52e09c73", size = 333673, upload-time = "2026-06-27T11:55:11.163Z" }, ] [package.optional-dependencies] @@ -1038,7 +1038,7 @@ sphinx = [ [[package]] name = "cloud-sql-python-connector" -version = "1.20.3" +version = "1.20.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -1048,9 +1048,9 @@ dependencies = [ { name = "google-auth" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/87/3f424bab34980b7996514d5232134ef868e412a9dcde1ba20345d674f62f/cloud_sql_python_connector-1.20.3.tar.gz", hash = "sha256:4b6f5c376982206fb0e62545c86d23ee49d045f2e71da817a326654cd169149a", size = 44211, upload-time = "2026-05-27T01:38:29.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/f6/cd4b630fca8f165db508795bc354f14e7155444a25a204b3da356134c3e8/cloud_sql_python_connector-1.20.4.tar.gz", hash = "sha256:fe2dbee747543ad2c720760c53064f0ef42ed04218981e1f7231362a88b3cf44", size = 44205, upload-time = "2026-06-26T23:04:12.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/ca/626917dd95d17eab155ad5f75360ca668928a0df63cc3259041ffd82ffe5/cloud_sql_python_connector-1.20.3-py3-none-any.whl", hash = "sha256:b4732920b5632be946921fa649a6ddeabfed9447a2b8088d55a6b08919ae3b85", size = 50101, upload-time = "2026-05-27T01:38:27.345Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/10a95226732a3d81ebcd157e5cb750b9d52a534e017dd177cc2321aea895/cloud_sql_python_connector-1.20.4-py3-none-any.whl", hash = "sha256:4c1cd8b573d5e9b93a6f390ccf772fa431afdcc32025b1577e2bafa89756a9f6", size = 50099, upload-time = "2026-06-26T23:04:11.098Z" }, ] [[package]] @@ -1847,15 +1847,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.55.0" +version = "2.55.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379, upload-time = "2026-06-15T22:33:16.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400, upload-time = "2026-06-15T22:33:14.992Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, ] [package.optional-dependencies] @@ -1966,7 +1966,7 @@ wheels = [ [[package]] name = "google-cloud-spanner" -version = "3.68.0" +version = "3.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1985,9 +1985,9 @@ dependencies = [ { name = "protobuf" }, { name = "sqlparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/2d/b857929745f57bb5b90f44970c02fdfbfb1184505ce4aa6e6c32550afb5f/google_cloud_spanner-3.68.0.tar.gz", hash = "sha256:90c55751cfc35bd58554c5715eab8be544095e21e40a805eb4d0c61a2bf07091", size = 904630, upload-time = "2026-06-12T18:03:27.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/78/0e3f0c59b38c31287c48c5b0f91976ec0fb0f0f244f8f8c4d1856db7a64f/google_cloud_spanner-3.69.0.tar.gz", hash = "sha256:e274dd63957db3c453bda20d25d9dc42662f2e58f53957bc3706811b3d539484", size = 904828, upload-time = "2026-06-25T23:39:49.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/f4/02ff12ebd23bb5af763b2b165deffe0dc78f933921903eb394a6ce4e0ed3/google_cloud_spanner-3.68.0-py3-none-any.whl", hash = "sha256:ad4aaf15e718fe0c54effbf510e1d9c7259f1252194c7192107848b06d8d2af8", size = 620018, upload-time = "2026-06-12T18:03:10.159Z" }, + { url = "https://files.pythonhosted.org/packages/7f/60/433929de67b12165c212bed01c976d9b79707500c521d343c1fb73e6ebc7/google_cloud_spanner-3.69.0-py3-none-any.whl", hash = "sha256:5efc8bc346fa4958135f43cd68c78cfcfa2c13aaa6bf162fa8b56943ca288ae8", size = 620018, upload-time = "2026-06-25T23:39:19.192Z" }, ] [[package]] @@ -2103,88 +2103,88 @@ wheels = [ [[package]] name = "greenlet" -version = "3.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/3a/cd99db55dc908568f6b91845747b98b3b17a06052fa1803d091dc91da27d/greenlet-3.5.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8", size = 285626, upload-time = "2026-06-17T17:33:33.231Z" }, - { url = "https://files.pythonhosted.org/packages/ce/09/fd997a19cbb97641233c7d5f8fc89314c132be2c8867c4f14beff979996f/greenlet-3.5.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421", size = 601821, upload-time = "2026-06-17T18:07:16.756Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b0/62abd204addd913ad9856e091f5d8baaedc7c85df151f22f093b8a207c20/greenlet-3.5.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574", size = 615044, upload-time = "2026-06-17T18:29:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/9f/5f/0f1db88a69c427e57091079b1478ae8e704de289b4f564ec573b3cdac38a/greenlet-3.5.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc18b8d33e6976804b9b792fe11cb3b1fee8b646e8a9e20bf521a429ddf73520", size = 621981, upload-time = "2026-06-17T18:39:23.961Z" }, - { url = "https://files.pythonhosted.org/packages/34/67/ceaab731b51611a8238b0af2d4abb4fd727ec09b16cd499fca5295603f46/greenlet-3.5.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3", size = 615176, upload-time = "2026-06-17T17:39:25.134Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/8c0c0768765eb57851bf65202e675e5ce6615fc4ce11d0e10be903cdc919/greenlet-3.5.2-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:2c6d6bfa4fdd7c39a0dbf112cdf28edbd19c517c810eefb6e4e71b0d55933a4c", size = 417918, upload-time = "2026-06-17T18:41:16.46Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/51a0ee73b72a7e4a65b54433316bbd7b3b7902a585310cd4e3051d411ee3/greenlet-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7", size = 1574580, upload-time = "2026-06-17T18:22:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/41/d3/a3a2163b1fe73042d3e72cfcb9920f2481d5188a1df2645587a9b83a903f/greenlet-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16", size = 1641192, upload-time = "2026-06-17T17:40:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/b4d83fb451e2f7266cb45ccef23857f8a800e0a5d9a73263fafdf7ba7904/greenlet-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad", size = 238247, upload-time = "2026-06-17T17:34:54.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/68/371ee6dad168be3386c46030bedaa8e3e7e3cf3d203621d4529e78ff36ef/greenlet-3.5.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5", size = 286925, upload-time = "2026-06-17T17:33:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/26/16/ed5706c26b4d26f3fabceb79abca992654eac8b0fa435def2ac6dbd92122/greenlet-3.5.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6", size = 606036, upload-time = "2026-06-17T18:07:18.538Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/f9c77093af9f5f96615922b7e3fe3690a9faff02adb89f1d74e21578b147/greenlet-3.5.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2", size = 617821, upload-time = "2026-06-17T18:29:41.317Z" }, - { url = "https://files.pythonhosted.org/packages/27/f5/a963a939039aa5acafc2f9535f6cc8958ad30afe1478e2e37ab5098af74d/greenlet-3.5.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1724499fc08388208408681c53c5062e9803c334e5a0bdaeb616228ba882aac8", size = 625675, upload-time = "2026-06-17T18:39:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d4/642833e778c17d32b5cabb793e14ce7364c55952462fc506fecdee55d485/greenlet-3.5.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1", size = 616877, upload-time = "2026-06-17T17:39:26.564Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c8/995a898ebbf44e3da0b7ea6fbc1631518c185fb83467a5d6cf408d6d3ced/greenlet-3.5.2-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:e976f9f6941f57d87a194c91868622c8b22a142a741d2fde31655c319133ade6", size = 420572, upload-time = "2026-06-17T18:41:18.035Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/7120f83e78b8be3cf7acbe2306b3b7bd2cbf99f5ad12e85e2f05d7b31961/greenlet-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f", size = 1577274, upload-time = "2026-06-17T18:22:10.692Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d8/05a0074ee485dd51c320fd706fd7ed48006b9cad3443092d7df1a655f0d2/greenlet-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f", size = 1643566, upload-time = "2026-06-17T17:40:05.452Z" }, - { url = "https://files.pythonhosted.org/packages/35/fe/9fe2060bdeece682e38d381184ae66045b48ed183c107ab3f88b9886a630/greenlet-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e", size = 238643, upload-time = "2026-06-17T17:37:03.039Z" }, - { url = "https://files.pythonhosted.org/packages/41/13/a9db72f5b6b700977ebd371d6a1f2984a08838357de924fcd5571607b1bf/greenlet-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d", size = 237135, upload-time = "2026-06-17T17:34:34.14Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7a/6bc2a7835731387ed303b9390ce68a116ab053df05450a59181239200454/greenlet-3.5.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", size = 288351, upload-time = "2026-06-17T17:36:17.019Z" }, - { url = "https://files.pythonhosted.org/packages/57/1b/bd98062fcef6d0e9d0873ab6f2d029772e6ea342972ae43275bd6177900f/greenlet-3.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", size = 604273, upload-time = "2026-06-17T18:07:20.296Z" }, - { url = "https://files.pythonhosted.org/packages/25/e6/fe392c522bf45d976abe7db2793f6ef4e87b053ebb869deeaae46aeb54da/greenlet-3.5.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", size = 616536, upload-time = "2026-06-17T18:29:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/cdb1f75f07214f13110e7e3879531f11c26083bd480a56a9474c430ec44c/greenlet-3.5.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87359c23eb4e8f1b16da68faad29bf5aeb80e3628d7d8e4aa2e41c36879ddedd", size = 621843, upload-time = "2026-06-17T18:39:27.507Z" }, - { url = "https://files.pythonhosted.org/packages/68/4a/399ff81fa93a19d6a9df394cef0355f082dbc19ad41aba9593cd0ad444e2/greenlet-3.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", size = 613749, upload-time = "2026-06-17T17:39:28.148Z" }, - { url = "https://files.pythonhosted.org/packages/2e/25/36a3628a7edcfeefddd3101dc88039c79721c5f8d688db7ebed1cbaaa789/greenlet-3.5.2-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:f4d67c1684db3f9782c37ee4bade3f86f5a23a8fcf3f8359224106018ca40728", size = 424889, upload-time = "2026-06-17T18:41:19.469Z" }, - { url = "https://files.pythonhosted.org/packages/a5/75/f519593f12ad43d08e28c03a95cfe2eeae011707dbc9dab0c4a263ce90f9/greenlet-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", size = 1573725, upload-time = "2026-06-17T18:22:12.023Z" }, - { url = "https://files.pythonhosted.org/packages/f1/bc/bc1ea4b0754c6c51bbf9d94677b0b1f7fbda8cbb404e44a896854fc0a940/greenlet-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", size = 1638132, upload-time = "2026-06-17T17:40:06.971Z" }, - { url = "https://files.pythonhosted.org/packages/36/c0/f0f5a34247df60de285f75f22e57f14027f4b3c43820981854b5b643ca6d/greenlet-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", size = 239393, upload-time = "2026-06-17T17:33:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/09/17/a8544e165445f30aea67a8d9cf2786d2bb0eb1b0e0d224b4d9bd80e2d587/greenlet-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", size = 237723, upload-time = "2026-06-17T17:36:47.776Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7e/28f991affb413b232b1e7d768db24c37b3f4d5daecc3f19b455d40bd2dea/greenlet-3.5.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e", size = 625044, upload-time = "2026-06-17T18:39:29.046Z" }, - { url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" }, - { url = "https://files.pythonhosted.org/packages/29/05/0cc9ec660e7acff85f93b0a048b6654371c822c884add44c02a465cf70e0/greenlet-3.5.2-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5", size = 427322, upload-time = "2026-06-17T18:41:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" }, - { url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" }, - { url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" }, - { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" }, - { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" }, - { url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" }, - { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" }, - { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" }, - { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" }, - { url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" }, - { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" }, - { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" }, - { url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" }, - { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" }, - { url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" }, - { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" }, - { url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" }, - { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" }, - { url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" }, - { url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" }, +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8c/231ca675b0df779816950ca66b40b1fa14dbff4a0ed9814a9a29ec399140/greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8", size = 622473, upload-time = "2026-06-26T19:24:12.786Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" }, + { url = "https://files.pythonhosted.org/packages/81/fe/dd97c483a3ff82849196ccd07851600edd3ac9de74669ca8a6022ada9ea1/greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71", size = 418421, upload-time = "2026-06-26T19:25:34.503Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" }, + { url = "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155, upload-time = "2026-06-26T19:24:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083, upload-time = "2026-06-26T19:25:35.804Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657, upload-time = "2026-06-26T19:24:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497, upload-time = "2026-06-26T19:25:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582, upload-time = "2026-06-26T19:24:21.357Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037, upload-time = "2026-06-26T19:25:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261, upload-time = "2026-06-26T19:24:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322, upload-time = "2026-06-26T19:25:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706, upload-time = "2026-06-26T19:24:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803, upload-time = "2026-06-26T19:25:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, ] [[package]] @@ -2498,7 +2498,7 @@ dependencies = [ { name = "comm" }, { name = "debugpy" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -2541,7 +2541,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.14.1" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -2568,15 +2568,15 @@ dependencies = [ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "stack-data", marker = "python_full_version >= '3.11'" }, { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/22/58818a63eaf8982b67632b1bc20585c811611b15a8da19d6012323dc76a5/ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c", size = 627770, upload-time = "2026-06-05T08:12:33.045Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -2598,7 +2598,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, @@ -2727,7 +2727,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipykernel" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "ipywidgets" }, { name = "nbconvert" }, { name = "nbformat" }, @@ -3250,46 +3250,46 @@ wheels = [ [[package]] name = "mssql-python" -version = "1.9.0" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-identity" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/48/0a/a05d39bd4aa7ec80bd716c9b4c10204c69b2371b0a92a0cca5eaabc4899f/mssql_python-1.9.0-cp310-cp310-macosx_15_0_universal2.whl", hash = "sha256:5352e9cfc999812494caddcfe9b4173adb19dd0b5abc97255dfc82356e590931", size = 28385584, upload-time = "2026-06-12T11:43:10.887Z" }, - { url = "https://files.pythonhosted.org/packages/dc/41/77899cb859d230153d3fd5eb89338444856fc79abaaf64615375d37df361/mssql_python-1.9.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:859cd3367ed315a2924fb272ab4e009c27ff8639f1b5f7806ef7970e6b6a9544", size = 25314140, upload-time = "2026-06-12T11:43:14.017Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/3cd38357a48599d579b45eca3a344063cfb1b10ba040c38802e847a0a788/mssql_python-1.9.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8114a77804e65bd53227efb280ecd2bf5f781df3961c5b2a8192240422993bc6", size = 25561116, upload-time = "2026-06-12T11:43:17.162Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/59b486edae895097a4b4e3f1149a8fc61cde66b87d78c39eedc174b2f65c/mssql_python-1.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f529ee3599a49cd4981b56e7a76551b92a9a155b8bf4e0f2978f51c05408090", size = 25249122, upload-time = "2026-06-12T11:43:19.717Z" }, - { url = "https://files.pythonhosted.org/packages/ba/5b/188673e5d54e09d955f7bfb083e15efbf0347f21b2dddf5485629a99660a/mssql_python-1.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b9ce68f8c32cba4751a4305294105873cf5a254c0a8e3af0d4e1beb4042628c", size = 25486931, upload-time = "2026-06-12T11:43:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/7debcd878f4c074462b60f6078b24d66b96ae320cf744e86b857a258dfb5/mssql_python-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea4e165058bbd3df826cb7a760868e673f5943746e5082b54195964bcc5e08b8", size = 15532190, upload-time = "2026-06-12T11:43:25.192Z" }, - { url = "https://files.pythonhosted.org/packages/31/a0/027a5e2ac1f1f2aa7c7d13ee43b12e78c17f5f207de8b1af09abd354ebb1/mssql_python-1.9.0-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:1bc6b45ca2233512ce0a0381b1f13ddcce27219ba4f8e24582f2cdb229446be0", size = 28387552, upload-time = "2026-06-12T11:43:27.818Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c1/d75aa4f99946e9d2a7f856195d6628dba81ef0bf8038fb30ab6dd4d5cc02/mssql_python-1.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3edf21b2dd77fda47af9703c082ae936104ff9b50c9d3a8e1d3ea1c44f2fdc27", size = 25817146, upload-time = "2026-06-12T11:43:30.45Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/c4ab00b7c69cb12257686e482f0b6888bc6533077cf6f8099a8720579704/mssql_python-1.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4cf6d6c10b3d6cf764e5cfc91d844772108f29b63ec3341aa3ae06debc0202cd", size = 26229692, upload-time = "2026-06-12T11:43:32.927Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/f30345b9ab955d9f368c23ab0d820f768ded58bd5ff3c845a73b3697d469/mssql_python-1.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95ac224c3f96b6cf34fcc5ada5b1bb33a16ad99547f10d2ede01d0cd5c87204e", size = 25690025, upload-time = "2026-06-12T11:43:35.505Z" }, - { url = "https://files.pythonhosted.org/packages/da/1c/2527a7a769f9c32852bcabfd028a80748086d06717d4a5cb57638b396d68/mssql_python-1.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d23b9871df79bdf30ef2eb0a1adcc4fc1a790d2e60c8a6721ce41c0f16940825", size = 26082985, upload-time = "2026-06-12T11:43:38.423Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/41e9fe1e526cbfc2256062b88cf02c08304e0f9a472b9cb7e50fa24bfe42/mssql_python-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f64e297d72b394fa9417f46141d81b84b64d1edaba6340593de76662636764d4", size = 15530518, upload-time = "2026-06-12T11:43:45.717Z" }, - { url = "https://files.pythonhosted.org/packages/26/2f/a4ec9db17e298c98b99b9c9484b26dc8c2fde2a3c09c7517f640c2c7bcfc/mssql_python-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:8c1a0dac656a5ce7d69650455a2ad7552fb5623785177e9eb79a9f107d266bb5", size = 18741462, upload-time = "2026-06-12T11:43:48.4Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0d/aec9d649fd9fee17c1aa6905ff320894c8059156c0fb1ca856afef91832c/mssql_python-1.9.0-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:dda283ebdb75facba18daae9297b4bbe0b554577a8594b4f7be4af6ed9595569", size = 28398509, upload-time = "2026-06-12T11:43:51.361Z" }, - { url = "https://files.pythonhosted.org/packages/bb/74/4f1d2c4ee0fb355229b3b2b20e9b806d6350444cb38defa51730984cc321/mssql_python-1.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9c2dc5542cd4ff96063dde229939ee929ab2ee14ae63bb5bb65a3d909df9c61e", size = 26315387, upload-time = "2026-06-12T11:43:54.29Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bb/2efa88ef73519cd7cc926156354e962c368bf65bf2a8ec7fd7b224d60eaf/mssql_python-1.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4ddc50fd10f968b22ca1156d799da6eac227c51ce21fec7f0a94b2636d0536", size = 26894006, upload-time = "2026-06-12T11:43:57.52Z" }, - { url = "https://files.pythonhosted.org/packages/70/c9/e287f6121e46222ee573c3570eb21672bab4d9c374a8a55a3df1f4bfea79/mssql_python-1.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5251efded6f4ccb950d9e69ab01b427c36ab67c4bfd08cd614c69cc22ccbbdcf", size = 26123602, upload-time = "2026-06-12T11:44:00.33Z" }, - { url = "https://files.pythonhosted.org/packages/81/39/1c29147960981749417c48addc4022b20e3c02840b3384cc730b83aae822/mssql_python-1.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e0117b9aefb309c67645a49f96592629e9f3d5e67fbf6f01838c725005c6b3", size = 26675359, upload-time = "2026-06-12T11:44:03.217Z" }, - { url = "https://files.pythonhosted.org/packages/fe/33/8e54f7eff3b9153ebe30527956d6e00119822c037b8e1494110ea51b963b/mssql_python-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:9642a80898d4f2f47a48c8bfb4706274c9a531082605e7992f34fce7334b7987", size = 15530857, upload-time = "2026-06-12T11:44:06.494Z" }, - { url = "https://files.pythonhosted.org/packages/63/ae/a02b65f858e911c83c2ec26fe02280c2ec7550dcac7fc57ad584e98b3b13/mssql_python-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:03f65679e42cc92ba060ac68e0253730fa99acf8aae6815de6761bda7c7a686a", size = 18741548, upload-time = "2026-06-12T11:44:09.409Z" }, - { url = "https://files.pythonhosted.org/packages/44/9e/8a8278577b630f73ea75dc0174424b3dca7fd589546d019dd6ce1bfebbba/mssql_python-1.9.0-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:f191e0a87c32ba4d95aafff5c1fc84ff9e8502149167f49dc9829f34daea714f", size = 28398186, upload-time = "2026-06-12T11:44:12.035Z" }, - { url = "https://files.pythonhosted.org/packages/63/d5/2e20d189cfe5d47b2b3be10f0f0ec4d5bcbe2a6db862c0f5987a6852cfc3/mssql_python-1.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:6278c0ebda116ab8c84969441d0b64b082f8415f9634f0f05c811d3dd22cccd8", size = 26817795, upload-time = "2026-06-12T11:44:14.785Z" }, - { url = "https://files.pythonhosted.org/packages/78/41/891de495f581c5881b65632d046f0ea1ec9c03e9fb2404644b94eac7c821/mssql_python-1.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:698c54667571c2a78699e45253e949f6d8f4edcd1fac987945118b9c683f6479", size = 27561909, upload-time = "2026-06-12T11:44:17.853Z" }, - { url = "https://files.pythonhosted.org/packages/04/2b/f0ff9ccfea2af82a67292c0d19f988a53683a086ec9a34ba5a7a2d13880f/mssql_python-1.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3511174978cfb853aadf264f73719f65e6982303aa81a8d326af2333ac69f616", size = 26560629, upload-time = "2026-06-12T11:44:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/7a/37/79bf6ca0e36a6456cb141675d9a4a09f9e48d25251aa638ae6f937567adb/mssql_python-1.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44a2934a0bfbe66b0c9b01c36e82de5fa8002fc8af322a4a905276cb257339f0", size = 27271424, upload-time = "2026-06-12T11:44:23.384Z" }, - { url = "https://files.pythonhosted.org/packages/02/47/61b37ee468f52b46d18f6a02523fa95a1b6bca263138a995321c6b0ac931/mssql_python-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:e351d50297c4b938a7903d890ae340da7ac273d874510b6388a2680190ac0ff0", size = 15530108, upload-time = "2026-06-12T11:44:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/01fb372b2a104e9c2ba7c6505eb100ed03b16517ce611c0ea0ee312a9806/mssql_python-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:640c37c749d7d81d7c03efa79a7000691f2192a93756cb1e7de21bf79ef82762", size = 18740777, upload-time = "2026-06-12T11:44:28.978Z" }, - { url = "https://files.pythonhosted.org/packages/71/08/04474c84908f457dd986ab82b0c98f828a1bfc24279e9ce193eb1bd5b6a6/mssql_python-1.9.0-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:c04bdef33bd266d4af6ae78094d44913de74baa5b74dd4d9f7ce94283a5365ae", size = 28391700, upload-time = "2026-06-12T11:44:32.014Z" }, - { url = "https://files.pythonhosted.org/packages/6f/06/b905959bf8ead7186e50da9efc5a1329c921c4e4000a427bd10aa79eba68/mssql_python-1.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:4593f0a8caf9eaba1590013a88cd21a0c39275f58a41706f58b8cb3be8710c5c", size = 27322484, upload-time = "2026-06-12T11:44:35.055Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4d/2f3322e1e479e12f9b4654b01a03606703ea847fdb023e334b1d3231fdab/mssql_python-1.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:30d66508daa199d55e9a052c9eae18fed3e60fbf8c0fba85e653c584353f8057", size = 28231736, upload-time = "2026-06-12T11:44:38.286Z" }, - { url = "https://files.pythonhosted.org/packages/33/50/7999f13dfddf697a5aef5d26185fcd39b2f06f558d5f0525dae86ef54272/mssql_python-1.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d85e11ee7b4d0d3f02c85171a54ce527f810740f9335af7e6878ca8093ac1bd0", size = 27000906, upload-time = "2026-06-12T11:44:41.135Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/1cccf0dcf839aa26a0136c080f2edb4f00830b2de4120a7c03d197f86a69/mssql_python-1.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8083c72ccd5198941133d743704c28955c09f36efd94c161ba0ed7004e0c9426", size = 27867536, upload-time = "2026-06-12T11:44:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/8d/19/e35ecfcdc6650266a421d484999906e97370a85537db7623afd09b388f8a/mssql_python-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:c2dd27191bb131f55b40b8814856b9d7e3d92defac250011ec04e8f9c31bcf55", size = 16055366, upload-time = "2026-06-12T11:44:47.36Z" }, - { url = "https://files.pythonhosted.org/packages/5e/f5/1738826f5a5e2bd11c10454cd82fa3b2df7f72fb16925587d9c68b77cda9/mssql_python-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:e6dd677dcc81af8ee117da4cdf844ad83ecdba0e1d6ac42f7eafd3fb77b64470", size = 19370922, upload-time = "2026-06-12T11:44:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/0f54ac796a5dbe530602d3cdb2c747c371729f9872c4646588e79740207c/mssql_python-1.10.0-cp310-cp310-macosx_15_0_universal2.whl", hash = "sha256:65957b7f97974f0874cf8c5c3123e87f58d5a47a4d93931c9758a92c23f9f3b6", size = 28321559, upload-time = "2026-06-26T08:21:20.835Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/aa0e47c7c8db56e4399c00ab47ba7c1d2afb02c59ffdc0f9f4996761b790/mssql_python-1.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:18c9b53c19737fa220b8fe95ad41078164be78eee676707e9262489e570504ec", size = 25305621, upload-time = "2026-06-26T08:21:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/50816fde85d88488a9f44e87506ad174a3f410655f642564f51a1920aba8/mssql_python-1.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3cf7e2f11d92dad1caa07dfd28339a8ccfbe701b4de4fa77eb6bda0a80109b5", size = 25538078, upload-time = "2026-06-26T08:21:27.272Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/9e4f9a8003cf960ed5e91f046d3eb37b4ae8fbc8169a3fd218c102f5337f/mssql_python-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b245ad4b1008298e8b32218d869b2551d522604de72d5814b8112e0a14896a91", size = 25240036, upload-time = "2026-06-26T08:21:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/25/bf/f7efba722b958185e2006ec79a39968a855d43458a5013e563abfde585f5/mssql_python-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fa168bdefaf07b3427b3f14c4c06c0cf5a196519c4bdff9cebec9705cf96154c", size = 25461927, upload-time = "2026-06-26T08:21:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/f4/29/fcbd37364dfd572ff2e3ed3f4097177254e0c84ecee4f3998d8fe9abc841/mssql_python-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0d418c3bfa9368ca24306abd7e79168acee9bdc0ecb2d8035698b9f3a9b3cb8e", size = 15522878, upload-time = "2026-06-26T08:21:36.935Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b6/3441bdb636a49d1c8b93d4087b7b99900728aa95ae17a7c59db323fbddce/mssql_python-1.10.0-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:e2ecbde000f8b524413becc0ce5da511bbaf4b4b1e72d6d47076e82805f9f6b7", size = 28327662, upload-time = "2026-06-26T08:21:39.766Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/7d24f166667417d82fbad2d4baf753780598f639c858e7fc60cffb72d055/mssql_python-1.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e8a39d7806fcea8ecbaa5696cbe4238df09c3eec45a3816aaf0776987e9f14bf", size = 25805926, upload-time = "2026-06-26T08:21:42.678Z" }, + { url = "https://files.pythonhosted.org/packages/92/4b/76835ca0ef90dde26991eaf94e646cc958c0e2a828c9dbe962ef7709c2ab/mssql_python-1.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7c5fccd888274d6eaa0538c3d7f10892aa6f17c6581a04ca783fe44f720198ac", size = 26204276, upload-time = "2026-06-26T08:21:46.844Z" }, + { url = "https://files.pythonhosted.org/packages/9e/64/563c213aee5c235e455f5158e548f28c16549213582cfead78aa916b7b8e/mssql_python-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ab460f61baa24c6bff6a1a2de91dc75239b180672bdced1215ba5b37a7ba90a0", size = 25678503, upload-time = "2026-06-26T08:21:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c6/4cab32da5b7e6e8f390cc81fb5ba3935bcd93b79dda3d319151727d56f21/mssql_python-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3b875a1daa707c85076034508cec4f934a865cac9f4f98730b1a98716ec48cf9", size = 26055898, upload-time = "2026-06-26T08:21:52.472Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ac067a24c484de34b525eafdf45814a970c7a3698790d8183a02c02cfde4/mssql_python-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:59f3a430d789c1b3b15b6e429cfa3412c78fb4ae756a11a0d871ab994bc96ef8", size = 15523008, upload-time = "2026-06-26T08:21:55.499Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/a362f5321cfd75ea6f729f15c18f6e6c600a1f72026082c25df56ee49789/mssql_python-1.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:ab4a9ac0b420f7b04cc4eedc418a403baf6f48f49f7752dc9aaa03fbb9be2d85", size = 18734312, upload-time = "2026-06-26T08:21:58.383Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/cc3e87cb4c6ea909f3e2d570396c88c216dd7360d84008d9d6e02e66d9b3/mssql_python-1.10.0-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:8949d73ccbecd5fad279a78b78ec522e474537e79ea03a5da40dd0fc28409546", size = 28326488, upload-time = "2026-06-26T08:22:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/90f9f7885648c5031c84aae7340554727087ae129c82ecda4f08a7530615/mssql_python-1.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa90c2b25f4b60cffcb07ab329b2e3cd01814c73c6890fb5a61ca2190fba2a85", size = 26299752, upload-time = "2026-06-26T08:22:03.98Z" }, + { url = "https://files.pythonhosted.org/packages/67/7d/83ac1f9c66d67ece0455144f38b1b53a24015af943679d2629382a26eecf/mssql_python-1.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c584689c720e92ce522f718c485cb18aa56f8cbb08da205d1d6b62176880f4fc", size = 26865344, upload-time = "2026-06-26T08:22:06.867Z" }, + { url = "https://files.pythonhosted.org/packages/6d/03/93074cba2046b7c65ed9161b8487bb3b591ee5caf7b519a803c0d7284d5d/mssql_python-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e594f74093cbd0ebf3a251cee1f04daac84655505a1ae512d98a54a4c60cb59e", size = 26107080, upload-time = "2026-06-26T08:22:10.023Z" }, + { url = "https://files.pythonhosted.org/packages/83/51/0f8474106671a70b084dbdb2993a4e88487b3547ebf0b66d63da8ae063da/mssql_python-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae16777095e35710c067147f2bcf41fcec9a1538a6df192731f15400fd3a4e4", size = 26645228, upload-time = "2026-06-26T08:22:12.712Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8e/0e0c75fd15fda8db965f517478f2152f56c3d3f0d830dd8358fc055e973e/mssql_python-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:74fef25e56d73d77965a3c727b51bb99c4fa25bde32e96e30bed3c6f77fb8117", size = 15519212, upload-time = "2026-06-26T08:22:15.279Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/26c5043c5b7a1a0a292634ad65e33eeab151d9d069adc552bba63be80909/mssql_python-1.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:38053e6d2dd8ae8ac096ee67d061dd6862ec164cec6bd67697e1f94d3c963966", size = 18729981, upload-time = "2026-06-26T08:22:18.426Z" }, + { url = "https://files.pythonhosted.org/packages/a7/29/dbdc757f4216bbb23f2768aec970c3c28ca7d61afde8e55fb6b25ffe0623/mssql_python-1.10.0-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:0e02f754d2c1641cf14e10068a833c0cc026ee7a7e839b92f20f32ee60726697", size = 28325494, upload-time = "2026-06-26T08:22:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/57/59/32a16eb963899cf5cc39d04b27c17cc4785b136563d76167109c66137b48/mssql_python-1.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:00653ed1ea90d3fd32d3f41332823039eb2ba71b3bcffad6b1e6a1e9e89ddd92", size = 26803264, upload-time = "2026-06-26T08:22:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6a/c228df6993f3686e66d1cc8ad76db1a57241c2ba6ddd643a4bcce740c2eb/mssql_python-1.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:717bb19ebb67a520c49ed75af7d1748fdc5c468769f402155521d5a493ebd517", size = 27534316, upload-time = "2026-06-26T08:22:27.835Z" }, + { url = "https://files.pythonhosted.org/packages/86/e8/b65bce44a14584319ab167eb940bd3d292423225c9aef20b436e2959337f/mssql_python-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6955c20d3be5af9f3373c035cb141a52603a2c6406728521a28a10feee283bbf", size = 26546137, upload-time = "2026-06-26T08:22:31.196Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/5f23a21ad9e8e224575dee56f9cf661928cf1166bb1b7ab9ddc2a6d633ee/mssql_python-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cc1aca1243ed5ec994a5116d791d27bd45e8e0b1dfd8048b4eb9beb74ba203a", size = 27242014, upload-time = "2026-06-26T08:22:33.97Z" }, + { url = "https://files.pythonhosted.org/packages/df/89/11c1cb560112fda7c6cade09f2cd0fcf1fc91d94331e4458b7eb4e92ccd2/mssql_python-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:651d6873ace3782704b116912ec4e8e7d76fcdfec9941398c82c33dad2f17705", size = 15519505, upload-time = "2026-06-26T08:22:36.876Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/391a336b96d75b183462f06d59c490677a95f93183d1cea873b468af10de/mssql_python-1.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:8bbcd14fa4f3ad9dfd3c26eee64825931322a6f8c74dd04fb70a0b23d7f4faa5", size = 18730229, upload-time = "2026-06-26T08:22:39.403Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/f8de0d278c6125d07f619d2b88a9e8742ea6f0037af9402116614d63f01d/mssql_python-1.10.0-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:31f30feda492de883e2440a1d2a6ec82dfc54d2f1ca64f90ed42a2918e30bca7", size = 28322330, upload-time = "2026-06-26T08:22:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/5fe873e0cb6572eb889c377d0597d070a41c3a8c78021e425c8dc12c4ef7/mssql_python-1.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1de5847a975c92b6f2cd62bf1a4117f3068455679e5149ffb1b3a6c098bb3d10", size = 27310969, upload-time = "2026-06-26T08:22:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/36/0f/8913684e667291dc5f861a5b979d5a5360c9d05b6215a20025702011f140/mssql_python-1.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:05a60e679b9e26cb53b99347f645489cd3ec6c7d4c1124fd08a740063267975f", size = 28205942, upload-time = "2026-06-26T08:22:48.712Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/db330ba8a7be7151246288bea68f71e86f1a708e534e70d8f24cfd3fcc45/mssql_python-1.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4b471ffee3149f2295104a98764b982145869d126aa59e084f8f35992decd125", size = 26989322, upload-time = "2026-06-26T08:22:51.994Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/876588646f20adcb6985b05f259a6f1bd1db17e19de9e547951f3bac19f8/mssql_python-1.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aaa1fe032183a14bed0131120f3db5defe930563b97f9b0a08adc6d963b13e2", size = 27840470, upload-time = "2026-06-26T08:22:55.059Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/aca98e2ab26f67ad793c7a48e7424cce49833e6f220db1fbb2230b2b33c8/mssql_python-1.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d4ce7ce09c9f49918a39a4d57c262bb77f97da8b1f7efa489df837b070365c3", size = 16047610, upload-time = "2026-06-26T08:22:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/fa65241815e4f748449a08400b3fa794f830e269888f7ceff4fdc3247c7e/mssql_python-1.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:368cd690198ebb5fc26d2827d797e8be30cf729a0d2e18d255680aa6bc26417f", size = 19363204, upload-time = "2026-06-26T08:23:00.279Z" }, ] [[package]] @@ -4256,7 +4256,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.3" +version = "3.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and sys_platform == 'win32'", @@ -4281,55 +4281,49 @@ dependencies = [ { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, - { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/5a/fd/e0194474c71dbfba82e744f66945c274f15b667acd5f8c117b12555fb91e/pandas-3.0.4.tar.gz", hash = "sha256:62f6062586d159663825f06e70ef49cd1572d45824cb63a9559f3ffd1d0d2a20", size = 4658146, upload-time = "2026-06-28T15:31:51.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/be/f23b2369adf0fd241820778e3d534e940cf052e6cd325fee9b508726d26e/pandas-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:87d6be6820c5c2b3c41d30f2c8387aac10e842af7d43dd9c3c22f2ce0a4c4176", size = 10409998, upload-time = "2026-06-28T15:29:45.196Z" }, + { url = "https://files.pythonhosted.org/packages/79/c0/902b30ae918f5482016f6151f7e4621b71f43200f4b20f7a7fa162c74130/pandas-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dce7eca7e5adc4cc79bc28435e6111474772c14c11f4a742ea0041e23fff7d73", size = 10041669, upload-time = "2026-06-28T15:29:49.492Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/2b287f9edfcbeecfd5ae9372827738f47f84130c4be73c9069c04e29bf1a/pandas-3.0.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14d274e00885373c879042dd3bd3dd27bfea6edef993f23644ed8a20468a7471", size = 10598978, upload-time = "2026-06-28T15:29:52.113Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/94e1bf5c6b3a635b7d8dc496af11840489322522a53e842477fdbcd8b08a/pandas-3.0.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c423500b3f0118c2d02b5a76ade4c57f3a13b9c9cecf04599bf983ba247cebaa", size = 11119683, upload-time = "2026-06-28T15:29:54.912Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/d8c41d1807a09c51170e2666c956e07d881eaaac1ecdc942d6f8662a9d1e/pandas-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaa9760a86a77a2586807a72f64748b312b95c37135ae9932cd65fe859507377", size = 11612249, upload-time = "2026-06-28T15:29:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/e62c97b2234c89bd541ff3cc19a019c4399ca5d0af35c6a35a6bf5b41a3a/pandas-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9c0eb4036cbf4110af16c5593191c51c1803752c7645f8f74b3a922a1027f42", size = 12170521, upload-time = "2026-06-28T15:30:01.695Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b8/f5f6caf9a6c643296a92e88922c53e9932101e00403517c45ca3704b47ab/pandas-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:a79a6401beed48057b9101d7e722d3d0af80e570b578438da6c7de4a10cc3a29", size = 9868871, upload-time = "2026-06-28T15:30:04.754Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f8/22d6f9575a918c0a5c2c57414fd4ff07765b8bce1bce34bb7b6a886c6d86/pandas-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:dc827bff97d448ced1a8d9b4055486cadd97b6ac52c3eb3e1dd154f49d869be1", size = 9117450, upload-time = "2026-06-28T15:30:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/69/7d/37fce6b1f4537218af19ab71ca2814e0794c4a1d4e412026afdc1988b5cb/pandas-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0f22242de864ca997028520e28b5dcdd440443ad239395bdabcb6124ed009067", size = 10422311, upload-time = "2026-06-28T15:30:12.192Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ce/3b280464d1c6faba9080435cd54f23c244ea79228f3508913f58753235dc/pandas-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bef5bf689a9f0d21ca83436f74c061ba52f426a689d2f8028e2e3d9ac0a8d05", size = 10061739, upload-time = "2026-06-28T15:30:15.162Z" }, + { url = "https://files.pythonhosted.org/packages/94/50/927a39cdf93bf541b56390c32a58ae64f70f2748b8f2a88576ce8e4b6d7b/pandas-3.0.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9624cceb273c8f97d16c518f0cbf362d12edbdbb4ed46712eb2def2f7ed7de1", size = 10287537, upload-time = "2026-06-28T15:30:18.181Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/6898384737b5e32dd72067843bea661661ff32325da91f978347ba563cf2/pandas-3.0.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a477e4b7f3d18d63b5131e7a5faea3f2f8d7153f493cb145e552ba52c904ab2e", size = 10793447, upload-time = "2026-06-28T15:30:21.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/1424ca0d99ea192e84b17d1547463ed7f9cd628c9c395b770aded58a7efc/pandas-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa604623167246500d962d109df5c5b953955afb637f0dd14d9500ab191dbce7", size = 11309351, upload-time = "2026-06-28T15:30:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f0/4f5aa9df56b378434b056afc22e0a6f4ed2b69006f61e61cd4b4397a87cd/pandas-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:833232f7a694cb841a2fd8b309ca655e24dcac2ae3e1959efed97d573499b389", size = 11859582, upload-time = "2026-06-28T15:30:27.365Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b0/97c3be4b0bbe82ed31d78d32659637d1ad0f2ad50b524a0851a068734f9c/pandas-3.0.4-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:25d9ba5ad021e7bd50c674f528e31f15dc22e28c9dfa50bd4cdbdd3a4f5f1902", size = 7115248, upload-time = "2026-06-28T15:30:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/66/93/503f381c106a9e6b4717356cc4805cc68b5af64b97f273590bf294cedd54/pandas-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:6b6797a68ccb1391ec7b7873681b23317dd4cfbd45228aadc0b837fdff02170d", size = 9670611, upload-time = "2026-06-28T15:30:32.776Z" }, + { url = "https://files.pythonhosted.org/packages/92/7d/9d7efe0c18d059d9cf0cd0f56f55c6aa584f8c506bcd5776ae097f195eaf/pandas-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:b3543e828bbd8d6ebe98b6a4268f57edc6bd945561fd08abfd29021bd5dc23ff", size = 8966047, upload-time = "2026-06-28T15:30:35.665Z" }, + { url = "https://files.pythonhosted.org/packages/dc/df/9febd45b3a643876260a4f53c6c1c7e30894e9e9f7d3a484b97d4ccc61fb/pandas-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ab8b23bf3ea0fe4337d115389d800896583854694b4c0b2de08e19c81f70140f", size = 10443559, upload-time = "2026-06-28T15:30:38.67Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e2/081b445177716989e6320c49b45c67b0b5ea75d71a327843826e380e1875/pandas-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcf15e8c8cbe4d1bb7b9c3f243ddb96123bbeb4585a40bad4c22dd673620a3c0", size = 10073663, upload-time = "2026-06-28T15:30:41.329Z" }, + { url = "https://files.pythonhosted.org/packages/7a/76/60d39cd44c42995cb92cc627138a429533098bb4e9f4268dfef53de24e77/pandas-3.0.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7ddfd571e26846b568d6e08065002046b54ffd4e81c51a61520f3040c677431", size = 10236941, upload-time = "2026-06-28T15:30:44.034Z" }, + { url = "https://files.pythonhosted.org/packages/50/68/7abd718ed1b373e47684bb788dec0b547ed218a41a15e0b04d05b81d1779/pandas-3.0.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afa70bd5a24cedf0c983d888f7d48c3ca1bc5fc095d38a3e6277e85f9bd4ab3", size = 10762008, upload-time = "2026-06-28T15:30:46.904Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0c/99ee1b43b23cbeb69585aebaa08575fa1bf3e8e0c5f3bb27f5148ae7d8bb/pandas-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba4acbe620c3888ffc561d278ba5a67f30fa9d2aba92052fbee9c951938110fc", size = 11258245, upload-time = "2026-06-28T15:30:50.438Z" }, + { url = "https://files.pythonhosted.org/packages/23/74/e6dd260c2d65fe8a5eed5b2bd7756f9393ccaefef12ea683fb05b7c15b9c/pandas-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26451dd26dcfa2e9f5172cb9c6d8213cebfc9130fc7a1844e11e642ffa458b54", size = 11820489, upload-time = "2026-06-28T15:30:53.539Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e7/8eedac9e7443651a4400d2ec8ebfb4be3be43cc3ec8ac3d5fbdbf7f0e149/pandas-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:63468a898ba7b54280fc6db21833cf99b9b62b2b54b5d5d10b5ab813ce100fbf", size = 9650344, upload-time = "2026-06-28T15:30:56.716Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/5f00ada639c67062720dc813a72d6dcca8eb86bc55620d2bb17dcf284073/pandas-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:5f9538b9d13b974a3a7d40c23b8d66f41114ceffe6c586f38efb8ed71d778caa", size = 8958077, upload-time = "2026-06-28T15:30:59.462Z" }, + { url = "https://files.pythonhosted.org/packages/da/9d/b70e2d6fd65f497a03922d0a0019a2b9ec7a40374fbd2f2cb6a697109806/pandas-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:45751e5d456788ca891167c6456fb5e373ad72aa82a949c9635b63e215940181", size = 10515604, upload-time = "2026-06-28T15:31:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/a514a959eeb0a8ad0294747df5ff1c95bf364aa2f2c533d6c2a8c0720bf4/pandas-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5c912d80243e4563934f34acc41cfad1a08d7af1b40597492cc6b5e6ba311404", size = 10169825, upload-time = "2026-06-28T15:31:05.34Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/11da3c5c7a39ab436bda99a1bf2ff5ab3b6addb91155b3fc5d068e733940/pandas-3.0.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccdf354cddecbf0d763ab5a21d0d698440154c5ed6f2c43643c5f528e5c184a7", size = 10384438, upload-time = "2026-06-28T15:31:07.949Z" }, + { url = "https://files.pythonhosted.org/packages/3e/64/01d6352e2f108045e628680f1dd0a11276489beeae580c2c9d8a74df390b/pandas-3.0.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadb8f48ada5de3feaa735577af15f0874cd5498d6875cf17989d97d4bc2e926", size = 10795673, upload-time = "2026-06-28T15:31:11.331Z" }, + { url = "https://files.pythonhosted.org/packages/47/1f/c4319dc17cc88b7a90780ecaad15a484ab8ddee32466093a7ff6fcf8fdb1/pandas-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf867a8fe99499762626adbda73759b0378add0d13a58044ee668ba2d5df92f9", size = 11394840, upload-time = "2026-06-28T15:31:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c4/5660f3f5bebfee4bcdee8c13212806f326ba9d1dbdf7f408c7766f279faa/pandas-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:58a0b88e5f6c46974ba110c2cef6b8714aaea57f9f26d55ab5361de662b28165", size = 11877357, upload-time = "2026-06-28T15:31:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/92/a16e08eebab6183817757adf6783f644e2061fa14e5905ced79d362cbf36/pandas-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:2a32675e6e7641effa300e58040609107f6e976bb0783b0264556358df64db58", size = 9806280, upload-time = "2026-06-28T15:31:20.496Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/7d0810b2ba48fd3b015f82e5f907b2b757f40727d86481a2bea22123e454/pandas-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:337be3b93ca7e0db3ec25edc769ebb74a8702427edeb345ee6ebe3e3080c6350", size = 9125456, upload-time = "2026-06-28T15:31:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/78/e6/32800039b35eaa4eeb2da0182dd13d2cc205ce21e13e860c0e855f9f64e8/pandas-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ca9c1332eb5b69027fa1a73125e5b35fae26aea8c64a716a1924261070b0b859", size = 10938203, upload-time = "2026-06-28T15:31:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/67/16/eff8bc63f22c8e881d31fe25a09542eea3bf8142ccf595974fc76f28a373/pandas-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1ae9e815bb61e0a85227aec3db22ba5502a4192df4918c115a772960e6ba606", size = 10586746, upload-time = "2026-06-28T15:31:29.836Z" }, + { url = "https://files.pythonhosted.org/packages/0b/03/6b413ed392ce8d7e90af963123bc62ccb6dd8d889ecf8951b7d1c1dddd86/pandas-3.0.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f482730bc2ec12b53c6205190deb195275495b4871bc1a2036beb91a7c97aee", size = 10240823, upload-time = "2026-06-28T15:31:32.879Z" }, + { url = "https://files.pythonhosted.org/packages/67/26/7992ed1748beb166a68d667e6651a03309f7e08a88a0520a7a381e06e21e/pandas-3.0.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5065f02eb94b947ff2610bba2b0c1a74fd67ca10fde0686b12adfc36e61d11d3", size = 10658793, upload-time = "2026-06-28T15:31:36.404Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c92b5ea088632476b831200c54de4740531a6777c8f650ab3c3cec5e7bf5/pandas-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b1a5e49e5fa13d002e1b237af4ab57f91f535cc6f6ca9225a366b2ce6241d99", size = 11274714, upload-time = "2026-06-28T15:31:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0c/149cfc617640575a28c878f2d26f20899f4ef206b08a7338a83eb592f297/pandas-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f660473c1e7a7ea7155214f89047b83ac0db6ebe7fab4d5a463f881307162d8c", size = 11729898, upload-time = "2026-06-28T15:31:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4a/5f1e5b5784ecd4eab4ad52b2025f3e828c289d49c063eac64b400619bb07/pandas-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:453c9e65d0ea8f1ff00d0d5dd2a2612df9ead3ac1baaf55b81e18a01a7a56846", size = 10201594, upload-time = "2026-06-28T15:31:45.917Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/40a59a8e1abf01b13be5102ece560dc30bb7e85ed06b3e78442c445cb028/pandas-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:02a8c70e5a304947a802551b8ddb38fe83d9c528395a361609fddcccb7cf8003", size = 9387615, upload-time = "2026-06-28T15:31:48.694Z" }, ] [[package]] @@ -4684,90 +4678,90 @@ wheels = [ [[package]] name = "psqlpy" -version = "0.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/43/cd0864390f4901d62bbfe38940eefeef1a462fd11db6724ca023bf1c54cd/psqlpy-0.11.12.tar.gz", hash = "sha256:207cc96a4265a682ae2054ad2f07ebb698fa111df3d7280958c0d5ce23bdfe51", size = 288849, upload-time = "2026-04-05T22:14:56.014Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/ec/c0f4184f7e2e1132fdc9604440f5722ad7488457ed7d422c90b5bdcc833b/psqlpy-0.11.12-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:86570795c36836cbc16577ab29e63996e2db142cad7d9696d0b539020f672f46", size = 4363147, upload-time = "2026-04-05T22:12:49.947Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0b/d1911b6d824fae14b0dfafc414a436201118b3f22611bd3bed39d68adbc0/psqlpy-0.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e5620daaef83b98fd0d95ed4ded4297a8f80c6ae361fe72ca89ee373d9186694", size = 4596277, upload-time = "2026-04-05T22:12:52.07Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7e/f9d6d027accb4cc26bce6efee3bcd021e6d262e59aa8de36f1dedef454f7/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:391d6d4057c85b696f904e17571d7410c931c1794c74ff6ae8ff75d71e9db638", size = 5100723, upload-time = "2026-04-05T22:12:53.78Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/e6e3c2347f46d6f1e5052b728c33499f47895fab1a8a38a57ebca8c6faf1/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f767be36248e2175af5db476d021d3d29e231703a64baad6c52b016a7a66093", size = 4378207, upload-time = "2026-04-05T22:12:55.339Z" }, - { url = "https://files.pythonhosted.org/packages/2c/19/9d9ab2369550c4c163b85041b0bb4f0d56ef8baf58c98bcc09d6a7a19c38/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d471b01108901ce8ab4eaf108c80b7ff13d742eb602818d0057bac8bbcffb26", size = 5010322, upload-time = "2026-04-05T22:12:56.7Z" }, - { url = "https://files.pythonhosted.org/packages/b2/91/5bbf50e8acf22c033c0310cb5a26db7c27542dc94db06681767793b75b8f/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:697c1f77ef4c88659eed89b0af35d84e543db3b3078ffd9bc985440ca97afc4c", size = 5030951, upload-time = "2026-04-05T22:12:58.344Z" }, - { url = "https://files.pythonhosted.org/packages/db/b4/8ef02da94d069b697586498f9f13b50c467b6da993c46aadd60890792ec2/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8d561bd1e2aa0a0921d1e29f23dbef67e189819c19c89890cbb7dc6bdc853e", size = 4722871, upload-time = "2026-04-05T22:13:00.046Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0b/1a853ad384988d29978075b5f74ea20084852f52f9222385411818b0a019/psqlpy-0.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42fb3237b32181a722d1169a9155847913f3e460f49232e7373ba808417168fd", size = 4881656, upload-time = "2026-04-05T22:13:01.564Z" }, - { url = "https://files.pythonhosted.org/packages/10/d0/47eab758c7ec8911336d30c5c8858ab2fd7f12567ede71d2da55ce712f7f/psqlpy-0.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dc9440664e5ed804cb9d9c374dbb210eb2ade274152734855277b5079d5818dd", size = 5071723, upload-time = "2026-04-05T22:13:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/7c/1e/952a38d5089113329de34cd25539a3ae687d3439b7a2f52b495b02ff0131/psqlpy-0.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6503b90273e0edd0c1aad75c786e5c28ac00278e27c83a8d26074b98ee85160", size = 5173138, upload-time = "2026-04-05T22:13:04.924Z" }, - { url = "https://files.pythonhosted.org/packages/97/b8/1f6a1dd6f05e89e84d112003773720de4e9bb9619ed57092be52b69fe478/psqlpy-0.11.12-cp310-cp310-win32.whl", hash = "sha256:dfbf6befa68edf4fe93049399fd896e16e3cb86f97cbf26df7e1dffb31f51f5d", size = 3591245, upload-time = "2026-04-05T22:13:06.34Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/38acb2394bf7c0ff81b89ae16a7bd8e7eecc0c97fa1dc62a527d863bd879/psqlpy-0.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:652ef081528aea1af558ae0ce2a79c65fa9a57354c7132390d53e946dad2f7f9", size = 4221678, upload-time = "2026-04-05T22:13:07.659Z" }, - { url = "https://files.pythonhosted.org/packages/85/4a/75efdaa2d853745c378e1995313f94b3d6e426e8d0c3060f9af4c085e1dd/psqlpy-0.11.12-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d54069af850b49cd23c63f28b307bb0f79a886d8afa3805837ac9e6f5d5690c", size = 4363036, upload-time = "2026-04-05T22:13:08.911Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d9/a544f6cd8ecd39588f064a0cec7bd30a7f502e278e57c3fc170657d826a1/psqlpy-0.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c09ca6a78dfe405e79ac471e335ed477f50524a07ed1583e9e06b478b477ae85", size = 4597637, upload-time = "2026-04-05T22:13:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/56/26/023b7d6cbe32853584657bbfb2d77a73d303ceac4ff5f70d92777fe4df19/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d618b755cfa01596cc4b7a26b6badbf37070e92197051924a7ddc1bd97ed5756", size = 5097082, upload-time = "2026-04-05T22:13:12.165Z" }, - { url = "https://files.pythonhosted.org/packages/ef/de/c62b1b3dc6b2e6737b4d01a8152331449d1e1458a923b0090c2d27d628a9/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:909cf2d0429ece957d2b7d350f17137d2a0351ca6f9a30b28626ad7b7c83b5f6", size = 4379087, upload-time = "2026-04-05T22:13:13.769Z" }, - { url = "https://files.pythonhosted.org/packages/ac/8d/7ae6436b8c28ee26719a7769123c401f02c6c3aeba112bd57fdb3f852748/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf3defd8fb9683d6c4e4d6f3eebd04e8c0b4b471d4f9b6fdc906f6823ad35eae", size = 5007249, upload-time = "2026-04-05T22:13:15.334Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ec/43aeefcf00207d72259baf4122a0117ff6e97de443a838700e36c38d2711/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ac0ed2111a7c465e3bd04a1b2a3dc2e963d9f532d796ab7713b2155cb418388", size = 5032624, upload-time = "2026-04-05T22:13:16.951Z" }, - { url = "https://files.pythonhosted.org/packages/85/eb/4f775e92adf1e0c669741722ae2668bb01a00cf6fba5cb8760a255911ef0/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6e645d7c9bb2cc2f8931d1c74bbc61d6df59eb27d07f0d56b5834346ef8190", size = 4720526, upload-time = "2026-04-05T22:13:18.526Z" }, - { url = "https://files.pythonhosted.org/packages/e0/cb/61011e930dac54865af28aa1b1b37381250f1a82f58f36f0b0222f0c2ba7/psqlpy-0.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42749ac188f7ecf5ef598233150ff6a4c58285d1896ab33301b11ae4af232c52", size = 4881807, upload-time = "2026-04-05T22:13:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/a96c7419d206a049e5523836f73e89be20b837708d7078c1e8cc31c317f6/psqlpy-0.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb4b62c5bc3f1cbe071faf34f5fe0d57cb53269a86abf818f879e430776740d0", size = 5071230, upload-time = "2026-04-05T22:13:21.3Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d0/3172b6f6a16e39ac68fc5e13b85ddfd30ad17028cde91ff82aed31bfdbef/psqlpy-0.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:02b6af79b8a388b67a08f88e2d88b8e95847194006e786eb357dac15568d789c", size = 5172129, upload-time = "2026-04-05T22:13:22.874Z" }, - { url = "https://files.pythonhosted.org/packages/84/17/a2a8301743add19a69f5edf1862bc53180e793e3886474eea0f98efa7e2d/psqlpy-0.11.12-cp311-cp311-win32.whl", hash = "sha256:844ae15f83b74d8458db61691178289cd22a11a3bfa859b5c2eb9765327c2e09", size = 3590286, upload-time = "2026-04-05T22:13:24.147Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/d5833a283918d536a8dd6faa4d8ccee34a9126536cdd648e72f93da1446d/psqlpy-0.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:8be2298647cb02a9cc8f5d16f818b20326bb3e80860c917a594ec56cdf95d3b3", size = 4221713, upload-time = "2026-04-05T22:13:25.589Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d9/ee1dc074c248e645981f1ba00152299487b92ce4e2ae4a8efc0b66d43fa1/psqlpy-0.11.12-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9b2008173a7e14d5999f054315e17026923dae39aa2981dc5743ec49f64d1a5d", size = 4345817, upload-time = "2026-04-05T22:13:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ae/6116dd8df3575962a8c62ba783391d6b680b55c1b793940d20ce63fd405c/psqlpy-0.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5ba88fdc13b34a1349a5798f6ffefb2877c6e05130575a42e770f10caf0570b", size = 4579566, upload-time = "2026-04-05T22:13:28.789Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d6/94815b04cf4d49ec03b6be27f4de077b69d73f1788b98ec0426f1119cb05/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d880fad4a1ce683fa1576b444b944c9b742217d1d5dfd6a7037db0863747a29", size = 5108251, upload-time = "2026-04-05T22:13:30.526Z" }, - { url = "https://files.pythonhosted.org/packages/13/97/e4fef0c3a8cba72278a634907e18210db159f35761bd55817008aa744795/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1535ca674e7f6f45f1d55098da6bde35534f86dc5b37ab866b61101d98f80b1", size = 4381450, upload-time = "2026-04-05T22:13:32.224Z" }, - { url = "https://files.pythonhosted.org/packages/d7/48/f3bfb918d08e4bd03c4479f4754bc832a714c14e1a9731d48508faacd111/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00ed1d6fcc05a21a395d2adde7a4f9406777722d3f6e6d232f464bc0c23d272b", size = 5015552, upload-time = "2026-04-05T22:13:33.93Z" }, - { url = "https://files.pythonhosted.org/packages/c8/92/458959237c95f39ff0e0f8c8933673fd8ee9a383798c210939d07ba770e6/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f19c7a115adb7e5bc6ab693df59eee563ec908607c27d6f8a03d1afd5a8a8a", size = 5028317, upload-time = "2026-04-05T22:13:35.671Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/f2bd9a6c8ef761a31c9bcb6dd9de7b1d496f2e569a6cf7bfc841269450ee/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d13676c24f598b7e421d47e0d02b68d6cbb7b9d3a92f7c70c0a0a82d05723b65", size = 4730518, upload-time = "2026-04-05T22:13:37.055Z" }, - { url = "https://files.pythonhosted.org/packages/4a/17/c996814e03e92cc0ceb21196134d21c63a65f44421387fbf72713a7d7d3a/psqlpy-0.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3fb0d5c9b47467afeb9881f65f125c6e2d6ca81efd7c57b1bf93dee6af3ef4e", size = 4885266, upload-time = "2026-04-05T22:13:38.49Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f7/7edea92d5e213a7b1c03013fbe6276b8423ebfe4c227958c31976254d06b/psqlpy-0.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:23a4a49e9f72d933ceb3cb78f799c9ba7ce8198aa5af815cfa5b289b243c1bb7", size = 5077816, upload-time = "2026-04-05T22:13:39.837Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7f/84382499c463334976a3393bc4ccb288ef484851877218326ab16c9978fe/psqlpy-0.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65fa8b7c65afe1b526f19ccab8597b0d698def99c01e12cd1606cc6f60133de8", size = 5181591, upload-time = "2026-04-05T22:13:41.366Z" }, - { url = "https://files.pythonhosted.org/packages/02/20/f6ac695851aec69ddd2d2de508e10190c9747d7dffb94a2e019c1012cf4f/psqlpy-0.11.12-cp312-cp312-win32.whl", hash = "sha256:ed836536a19bdb1042002f14e0d844456c6d64ebdb2870f83a4003987d8f8839", size = 3586164, upload-time = "2026-04-05T22:13:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/af/f8/67c683593fc43d9dfb787693e4a0909846c3df928cdc790a2acd4098d692/psqlpy-0.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:46e4c650febf3c1088b9896f3dcc1267e9d03525d5eda93990f6561251372ac0", size = 4226168, upload-time = "2026-04-05T22:13:44.73Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/cf49896e5ee2a733bfd663610b4b8767f20f612aa5cee1da17d6db9f018f/psqlpy-0.11.12-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4cb98a1b8b0dd321876d9d8eb6b0dda039e348d3ae90c4e80f8c56d1e6c44d71", size = 4345907, upload-time = "2026-04-05T22:13:46.211Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0a/89b8326fedb2453c20b4e4ece123a0be4ac4a7ed4eb60804e17fc8d3a22a/psqlpy-0.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee63d7bb59879528ca509f4a578b824697e44f5006900418d8640980f59e452a", size = 4579510, upload-time = "2026-04-05T22:13:47.598Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/2a48cba76c76033a20c443c4c0e90b0c427aa70ae7285a1665a7ee4eba17/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b848beed642e3c8e68b89175b128dd73263eec0c5dfcc38bec527290980e8047", size = 5105802, upload-time = "2026-04-05T22:13:49.333Z" }, - { url = "https://files.pythonhosted.org/packages/f2/bb/4044cb7b48df7eba2667347206e8cfe96f46eb7bf0e4c128e4db1ccc5e2f/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:299f6e4f38eedc3426b7f29923b6986c04bd1c9c9c403d6b80fb83acc554b588", size = 4381115, upload-time = "2026-04-05T22:13:50.996Z" }, - { url = "https://files.pythonhosted.org/packages/8f/09/6afefdaf739b908701de2533957b48c18a88001ea3525edc72816b19748f/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee3e77efaef7dfae6edfb57257b9dad3e4205b4330391ced75aefda42bea8512", size = 5011827, upload-time = "2026-04-05T22:13:52.425Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e8/338d17d3bcf7aea00510148090d08096053a982bda905eb7bbf2e32db53f/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1870b955b3f45c99f694e78992a0588ba4a64992c2e31345af328aa2e9b291e4", size = 5027469, upload-time = "2026-04-05T22:13:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e0/cd7d9f427d3fdda1859d0e88524ad1b7897ec0057297e83c302ae7e70a0a/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:795849a91925da05853f340148edce7f82003ffb3c588e0b52bbfd6ab0fdbc3f", size = 4725558, upload-time = "2026-04-05T22:13:55.707Z" }, - { url = "https://files.pythonhosted.org/packages/57/33/7bed9f72488a668ccb6630f40267d6bc6cd7ee84001bed5fa5d682908b40/psqlpy-0.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c101ea1fe32de8f0fe282b3270b22b677493c71496893efa8850acc5eba0f50", size = 4884666, upload-time = "2026-04-05T22:13:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/75/94/c5717128147d748e8f6458decca7e741373c6a3ad5364025b1ead6fbb182/psqlpy-0.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9fbcbda65a58112647133474c95c3709679af2c2c2349d497c5162ebe332d3a8", size = 5072808, upload-time = "2026-04-05T22:13:58.533Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f3/d4bb78c559c04adfe5642584db08701e031ff31f093332cedafec259592f/psqlpy-0.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e47eb07e8edbe70e22eb2a6ce0815133b68cacb5a19f37cfa07b72574616b5b", size = 5177571, upload-time = "2026-04-05T22:13:59.992Z" }, - { url = "https://files.pythonhosted.org/packages/ae/91/d35ccbf603c46c7c26f4220a36e91aec01d7949b8a1b4384a0889e142e81/psqlpy-0.11.12-cp313-cp313-win32.whl", hash = "sha256:a52d995af9fd354509047ddc58a959d00901bec071907530a8dbda91ca023a75", size = 3584480, upload-time = "2026-04-05T22:14:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/54/04/776289306b3b281f15995ef454509dc3b8f427065822e2e943e56266688a/psqlpy-0.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:ccb43308e8ece0bdb8ebf1201aa45bad4b56aaf1d5247cf618734723c67e0e18", size = 4225604, upload-time = "2026-04-05T22:14:03.111Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a3/e690d44f91eca68f3c8fbe7f855986fc88cfe301203a28ef46a0ef743966/psqlpy-0.11.12-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:78d0ce8d3d59bb03c0083eba15567560b246eb6198b1091c037734794e616b14", size = 4342327, upload-time = "2026-04-05T22:14:04.413Z" }, - { url = "https://files.pythonhosted.org/packages/5f/31/ef0ced480b68904b82c1fc1a6e0e27da0c30912c89f246371cf3c2b02d34/psqlpy-0.11.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f9364e9de88078503b0535227e9e23d51455480bb7cefe1e4d9201a6fbd4b2a0", size = 4579228, upload-time = "2026-04-05T22:14:06.103Z" }, - { url = "https://files.pythonhosted.org/packages/ab/32/c75032d7ce4746d5b9f27e7f54429bd9871df3c28ba5139570a7461ac50b/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75d912016129ec58f2ed32b3686ecf9c3deb8fffae726e6c907f45776ac833ee", size = 5101903, upload-time = "2026-04-05T22:14:07.576Z" }, - { url = "https://files.pythonhosted.org/packages/d8/60/16182558ad7a28933760446fa4f64d80710bee9f019c7391e67d9871f39d/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c555f9bd569f2fa88ff200c48703bba1a59a5cff17f7fed249234bdb7ba7490", size = 4375792, upload-time = "2026-04-05T22:14:08.942Z" }, - { url = "https://files.pythonhosted.org/packages/52/34/402f494f3acf303cf8cd5a0252ab4fd88bfbfcefced01926941ebd3d6cb3/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0731c2607d78aeac2c73b5f9b994814a8e607cddf9162135f07124c80e40c0f6", size = 5011462, upload-time = "2026-04-05T22:14:10.383Z" }, - { url = "https://files.pythonhosted.org/packages/26/ae/271ad1120c0bbaf58fb4c7609a3627660798997f756812e6ecad1f8afd9b/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a512ff075ec93a3f37ac1f0508616cc88203cff76ed6e869507748f707c4f156", size = 5033304, upload-time = "2026-04-05T22:14:12.063Z" }, - { url = "https://files.pythonhosted.org/packages/15/62/7eb83c5a989ab1734551a8c6144d10ebc8123373f9e9bc11d0c8701f3ebc/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec4c5e7654fab06cdd7695c8ce2428a3f69cb418f7d55c1bd1c3972f96e45ce6", size = 4733495, upload-time = "2026-04-05T22:14:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/59/94/e9bc82a71a57212244cf3828e3cf18865acda680c1366e9fe4942a449e60/psqlpy-0.11.12-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a978b895005243822714cdbe915d34fa89ee3b07359d9f4dc26c6140e64bd8e1", size = 4883486, upload-time = "2026-04-05T22:14:15.217Z" }, - { url = "https://files.pythonhosted.org/packages/71/10/94e7e54a0e9f63a72df4e04d8924246301e12d36187526a7a525ef8691c2/psqlpy-0.11.12-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a410870f2d13a4d1cab6445b577ae116173ae03b6726b22e66536a389b5cb803", size = 5073835, upload-time = "2026-04-05T22:14:16.684Z" }, - { url = "https://files.pythonhosted.org/packages/2a/40/19ab0522f7f8f682879cb72d8c50f0e7cce91c8342bb07b45e330841d591/psqlpy-0.11.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:440170b23c8a3e333a8b4cb08a9478c80ec57695e3ac88be7fbfb1948570a960", size = 5182109, upload-time = "2026-04-05T22:14:18.996Z" }, - { url = "https://files.pythonhosted.org/packages/7d/30/092340f4acea8622a330362b64a657981513b20490686d1f2340c327bac2/psqlpy-0.11.12-cp314-cp314-win32.whl", hash = "sha256:2982e651effbbe96dc8282682a4efead2c59211fc2c5de2a324a10adfa9e190f", size = 3589691, upload-time = "2026-04-05T22:14:20.569Z" }, - { url = "https://files.pythonhosted.org/packages/bf/73/113fefdbb86ddf1c739e1441cb5ff6341b980e1080617170766894667589/psqlpy-0.11.12-cp314-cp314-win_amd64.whl", hash = "sha256:fc87a7514f27261a72bfeced20323d2b3f2bb41c25043462f59f85982e0504d9", size = 4226495, upload-time = "2026-04-05T22:14:21.937Z" }, - { url = "https://files.pythonhosted.org/packages/34/3c/118e9eec52bcbb915b6172211aece5de75f1ba5ca454b738eca66a3a19ff/psqlpy-0.11.12-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b33b3045edfe01998f4c1191169f2d3ec078478a491f0f43576f9552cde82cb9", size = 4361358, upload-time = "2026-04-05T22:14:23.231Z" }, - { url = "https://files.pythonhosted.org/packages/10/8d/9575e06f3099a86f9094f1b50c244b586a2f1d43bec41c30ccae01b6906b/psqlpy-0.11.12-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:53df72de33c80b5a4bf9a4c06d45654279c9ee13019894e3662afc7d21d46daf", size = 4597551, upload-time = "2026-04-05T22:14:24.854Z" }, - { url = "https://files.pythonhosted.org/packages/9c/29/a841a456173770b399343579a8b8873a2035a3f0d5b83eb8ff43a295d1d7/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3143ca17b3f1fe03636138cbff46de01983065f79cab1d5b0da4c98159ad2e68", size = 5105740, upload-time = "2026-04-05T22:14:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c5/8597f253370fe3e897a015852dc34d8994c297f05e030686cbd9867c80f5/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:191a0b5b72fbaf3fe2d3e062ac3e5b86ca91233dc5070f3229b7a574a0f780d1", size = 4380912, upload-time = "2026-04-05T22:14:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/d0/4a/0e20aa9194ce553c42bd81f01e8a37668324e0cbda46ecd2b9268a1049eb/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a2da697119c08d24f8d6cfaa595c6fa8d850a66c1c1c9125649f60a1eea2bf", size = 5013948, upload-time = "2026-04-05T22:14:29.579Z" }, - { url = "https://files.pythonhosted.org/packages/87/a7/6deebb0891166ba3ad098a66b70677bc052fb6b408f93bf4d201a4a42e42/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f3aad002cc534fc53e1a08f9e1cac491ceb2afdcec2805fd83af38943ca0246", size = 5030940, upload-time = "2026-04-05T22:14:31.45Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/b07a5b56faa77a04b11e1bcf9d8df62cad461fdb61f3c5c734e8945b1025/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0baa560e136598c72c59d27d618f3f2556899f1af950fa3a8950461e56e766b6", size = 4719457, upload-time = "2026-04-05T22:14:32.983Z" }, - { url = "https://files.pythonhosted.org/packages/4e/4b/7d3e420c59620ce82298075dbe017a61be714558492f13b4e7f20fcc300d/psqlpy-0.11.12-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ed20e993fbc95ed19e3f2ef39eede07b0702c66c06a8d163009c73246cf87a", size = 4885707, upload-time = "2026-04-05T22:14:34.474Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1e/9b8e0da551af49319122d35882bd978aaf388abe7f0f71c00ec8693a07be/psqlpy-0.11.12-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:5e4453c4e2cd30e2c11450378c4514808c6d6ad2b4b7c292330698bbab56455c", size = 5075795, upload-time = "2026-04-05T22:14:36.405Z" }, - { url = "https://files.pythonhosted.org/packages/5a/32/e28d9516728efa24693baf379be92468d48bc9f5de0393e19ddfd3346f86/psqlpy-0.11.12-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c1ee401eabbfeceea58a94a42c84e8304d3937c39bdba30dc68af255f948dd70", size = 5177786, upload-time = "2026-04-05T22:14:38.032Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/896f2b0f12dda3505d424b30fdfcd30169903735a3fdf492a0bc9b04f738/psqlpy-0.11.12-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9366a32ebdc579507afa62289313b89e6067e4f7c1c21e3c4af27c37db771e4e", size = 4360509, upload-time = "2026-04-05T22:14:39.875Z" }, - { url = "https://files.pythonhosted.org/packages/40/7a/3b7ecae78168bd140d77bb46c550f3ce196e0dbfa6e0a567e2d73b273b81/psqlpy-0.11.12-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54bb660b51efb65de587a87f56a37b14673aaaa360e46fe5fbd298c82a35f6bd", size = 4597477, upload-time = "2026-04-05T22:14:41.608Z" }, - { url = "https://files.pythonhosted.org/packages/10/49/cb2d877f30bd7aa1933be4fbfbdec690b325c393386d488b24f511ebc309/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34b4263f9195a755f39fb2c3c8a972627b85a355fb97ac93006270f57dc376e0", size = 5105867, upload-time = "2026-04-05T22:14:43.116Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c5/c0de7d37fa6c53d5d2db297ba6c719d0d7bd6a1dc8aca1cb960a63237c40/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be793ae0234cb59dc1a90b61efcc8cb9415a8356e9eed0102b114dded0839c1e", size = 4381331, upload-time = "2026-04-05T22:14:44.864Z" }, - { url = "https://files.pythonhosted.org/packages/b4/83/92b37a701180889b7f5ac60cd2b476428cfc276d2ba4b64e28b961151686/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46fbf1395877ed76ddf89b69da2762d102cde876d72c224b983838875be0e9c", size = 5015059, upload-time = "2026-04-05T22:14:46.501Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/aea3088a3933e5a9d87c8cfcc465539828666d72e28738204f69b241a592/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fef9329375277181ab46e2a0d5613fafbd22269b31c86afb5da40c2d6d0810ff", size = 5031647, upload-time = "2026-04-05T22:14:48.108Z" }, - { url = "https://files.pythonhosted.org/packages/78/5d/317cf5d45f0cac680c97fd8b0b32caa27339061a348cc2c1664973673a32/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:222d36b03f5dbfc417f21ac930b14e4aaee6ee177913f782f4e6da39d7d53170", size = 4720479, upload-time = "2026-04-05T22:14:49.854Z" }, - { url = "https://files.pythonhosted.org/packages/1d/e1/c62a3919c99679eca8319cf4bb4729e48124a31ea3fec896d3581b2d4179/psqlpy-0.11.12-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee14c79ab6c5e4b9f3ec1bc95e56e9e3a14e9ecfb4d426b8e62de8f57928b1f5", size = 4885482, upload-time = "2026-04-05T22:14:51.371Z" }, - { url = "https://files.pythonhosted.org/packages/af/ed/b31211bfffda7dd90285d4ac5d842ae5ace8c42e8c1429bc395ff560aa08/psqlpy-0.11.12-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1436a058e78d5d3be97b088d78a1d7c093d4feb9e659e7a22abccfee2ced2c10", size = 5078033, upload-time = "2026-04-05T22:14:52.884Z" }, - { url = "https://files.pythonhosted.org/packages/dc/12/6f8f0b6d0e51998ca10720cb39f19c4223fd948bf666482f7d9cc88ac4f2/psqlpy-0.11.12-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f42f6235a778ef6c5ad7a5b387f373ff2c23017afbc699398dfb809b9408890a", size = 5177701, upload-time = "2026-04-05T22:14:54.56Z" }, +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/e8/7a55ccd9f0ae0343b40394cd2dca45c3e8cb8308c0f89bc19367458cbc2f/psqlpy-0.12.1.tar.gz", hash = "sha256:d702874ef5498671ea1528f8fb4ae299f87983c0f88ea9592f0a40df6263ef66", size = 306171, upload-time = "2026-06-28T18:43:10.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/e2/02c96959e630c5135b1b59d21cc0cfc65a6b288c88fc10eef4175a68ba97/psqlpy-0.12.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ab3aa5975502a7596874f6282c1afa6e6e4b9e9784c2018823d5c5d05b5df024", size = 4426838, upload-time = "2026-06-28T18:41:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6a/a605f9d09f430dea17aa4a46163b7bc9dbe3d823486dc8ab829ace26d2f2/psqlpy-0.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0295f38327e9b06c017f14ded3f8b69827d73ba1de155c70f526ed1800ede619", size = 4644985, upload-time = "2026-06-28T18:41:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/28/5c/f511aad98a3f4573b9cb66b05269caf520081bc5dfd593023dd28b9f6417/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79d349778bd6ac614d5e4ec5150b0abb3d54ebce35edde7097e9295f67e5970a", size = 5167533, upload-time = "2026-06-28T18:41:04.96Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/3802c1303675e2498caa1446163a4cf8c9fd3e3c626086b87b61396dd327/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9c4bd6a08f92afc9c9b31ef8e93570a5ad6a5871d95b2c199a53f37c0308a62", size = 4437426, upload-time = "2026-06-28T18:41:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/16/2a/50b0c1574c390d8ed729e4edb5e70d718a6af724dae2098d0d0f5c69542b/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04b30aa735ec8f1fb35d8203f5cfaab1871ebe989cbc815a60878c173c440742", size = 5074396, upload-time = "2026-06-28T18:41:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a7/5cf32454073d2774ac0901fa4ec0ec977db85428ebb4a93847b2f6a5a018/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30422812e5de77fd80362cd6a55c5e620dd71df89e5d90dadb8fe98793f61264", size = 5091560, upload-time = "2026-06-28T18:41:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5d/f8448b9ee0a5ce8498191f54e41fa49ad7f6a9f03e66d0d7ad30e278af5f/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4c4e76cfc7643c9ea5a02930827a9e74da8bce11d5d08b3f87f6438a24e8b46", size = 4770859, upload-time = "2026-06-28T18:41:11.992Z" }, + { url = "https://files.pythonhosted.org/packages/74/b3/358c2f49db6f89ceefc0462c7a6004cf30dae5c1e4771c87782251fb2ecf/psqlpy-0.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e1a9d2f59a14a479e08bfd1c166e27d0f05fecf5d65550cb01c1b36e46bb76", size = 4941929, upload-time = "2026-06-28T18:41:13.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/feca31531d82d1c8a7caed45afcd74e504c973061099544cfc83aff92b38/psqlpy-0.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e7714d560477e6677959ca0bf7d93bfce22b279f0d8663775ba05b99f2204c6e", size = 5137323, upload-time = "2026-06-28T18:41:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2c/5572fa1fea8b852ab17de774f0dfe1850162a74a456992e119c7e2f8d4f7/psqlpy-0.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d24d30dac8710ccb1416af660ad3da7328094af281e58c457a797d9ff09f865", size = 5244595, upload-time = "2026-06-28T18:41:16.46Z" }, + { url = "https://files.pythonhosted.org/packages/85/63/01d6442ab9cfb97d07380309d48e1f844a60479e28dc957e3b83ccfcc5df/psqlpy-0.12.1-cp310-cp310-win32.whl", hash = "sha256:77cde1400396c0a33fb59af7bda1df219e9a8684758561e2a33f53e5a3c9d924", size = 3641507, upload-time = "2026-06-28T18:41:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/46/bf7f783141efe4a195afae33d070a110f69b3b192bd9e6a49161ed0730aa/psqlpy-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:4da20b1ef1c40f936b3d564e219f7391463a6de87d2aa650f01f24a5b638c817", size = 4287934, upload-time = "2026-06-28T18:41:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e6/9b46aa639882cd601da05fee55a23ff0f010b1d0572a604058f6a97abeb7/psqlpy-0.12.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4dee6660a4227b4a8ddb94c427e97e99d47dc564c649827151c8708f2bc77f7b", size = 4425370, upload-time = "2026-06-28T18:41:21.303Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ba/829bbced20c91f55122207dba5618ecc302a08fbd3de17fa4c485ff18121/psqlpy-0.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:500ae80c01c3c3f646f5f9a959f1aa9817b6643c81ec2d4a3164928655cd9863", size = 4645861, upload-time = "2026-06-28T18:41:22.708Z" }, + { url = "https://files.pythonhosted.org/packages/46/12/081913871867c342c9adacd5847d93c12226d3acd8f7974ba1dcf1973eb4/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e1cdd3ef44757ef32f225a4aaa52db3daf25df1c84ef560f43ca0ae7d060054", size = 5168221, upload-time = "2026-06-28T18:41:24.458Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/e2a2e49ccf38d857358ef7c18692ea1cced71920b9ad4671776e7469038d/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:860cb87fd26491634a994f76f9d6ab11d0aa4e44ba4b2b532ab9b5623a6bf9ed", size = 4437098, upload-time = "2026-06-28T18:41:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/29/54/6d89888acf1bfb78c6cbe51db59c926c42445e164d75d9b9c69f86c6afe7/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f58d4a635472e892f894cb8275a093678dd739c44c916fcc74f27fbb3121ae2", size = 5073984, upload-time = "2026-06-28T18:41:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/18/2d/f915a0e55630566976da61bbd301c426c01cbe9fb8f2e8e4e3f9b96561f7/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7ef0f6249ea107d914573910a9f91da824988a359af7d23c27f274b4061676e", size = 5091085, upload-time = "2026-06-28T18:41:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/f21184d43539c287407987e17fb7577fca51761a9f0aa1c68610d251c3b3/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca12123c121220a8abf55c2a5a5004bbfd3396bc3d62026f441f0cfee70b1d0", size = 4772363, upload-time = "2026-06-28T18:41:30.65Z" }, + { url = "https://files.pythonhosted.org/packages/60/ab/1b93ddcbef43a05b8b9a3cf955e120cc74f9163965925c8cdff5481c48b8/psqlpy-0.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d482504292711c9b4835b7f5c7e35691809128a6aa3e4ae483ae045b97678b3e", size = 4942933, upload-time = "2026-06-28T18:41:32.368Z" }, + { url = "https://files.pythonhosted.org/packages/45/e9/08538d8db5c723c7fd2debf9e8c3fb6f64819b88db756881afe519cbf860/psqlpy-0.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f137f4e0e3ec7ddb8511d6a48a65b14c250eb82ab43a1af775122d5d4c890099", size = 5137091, upload-time = "2026-06-28T18:41:33.954Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c3/921c5c38eb1e783c5ba5e115a9ec0758a75e22214d16e59eceb7e4d4cc46/psqlpy-0.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:528dd4de3f05be318a6b6e21fd0c143555573da514869d4dca970bf16e35f220", size = 5244783, upload-time = "2026-06-28T18:41:35.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/a7/97a1d5069c5ee0724f9829b482f654c4347033841ac9c94b7da8c7491f2d/psqlpy-0.12.1-cp311-cp311-win32.whl", hash = "sha256:8a2f3f63b73702202c456b3274134f2de1b4701e2cdebe3ed737596554987e00", size = 3637338, upload-time = "2026-06-28T18:41:37.174Z" }, + { url = "https://files.pythonhosted.org/packages/1f/77/1d8bec8ad21d7b3c5e456eef174af027fd335dc183c1034520c24681c8a9/psqlpy-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:271ccbb8d6eadc74f9c2756cd9edc327c78cd5b288c20e04ac0b0c211f472f6f", size = 4287828, upload-time = "2026-06-28T18:41:38.897Z" }, + { url = "https://files.pythonhosted.org/packages/19/33/0146718779fc93a51e9a4bf9d354cf4bb1257d9877b0bbd788e7dc59aecc/psqlpy-0.12.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:17c26d42e0fd251e309da676f54b7eefdf666690ce3e88897ac0dd78555c060c", size = 4410242, upload-time = "2026-06-28T18:41:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1c/6b57dd9f48dec3e61f4ba54d2c7ea7fad66d8e26b618561e11bc09657380/psqlpy-0.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:98e729fd00b4564ce982ef69f7397af5df27865edf7fddf887cca597a9a34967", size = 4630762, upload-time = "2026-06-28T18:41:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/d055aff6aab84992882e6c4a9842feb3f3cb625c86396cce7d1cbe2ad4c2/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26cf01dbc6f9946af748085b712c1b139796bb5561631285de4b0f388dbdc8be", size = 5172881, upload-time = "2026-06-28T18:41:43.619Z" }, + { url = "https://files.pythonhosted.org/packages/44/1f/c4b74f743f1b24e9a0a0aa24618988ad51531a1884994e54a48d433beef0/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a466c9fe22bab8173d7aa80609afa018402c8c79c23cf6494500560f87305313", size = 4450123, upload-time = "2026-06-28T18:41:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/14672c2fe1adda215c9bf1a7b4f0954a0f80a18a854d5fe5bd52afe683fc/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82aa541906cb2a2388785f3688a03aa296762a8e9bf0b5fd38c044866d8a1b3b", size = 5084373, upload-time = "2026-06-28T18:41:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/99/f3/159008c6d52192debebdcb05d36be0ed12a99fb38f1df756b47ee3dbd67b/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfa69746bc96eb931b9cdb8a6de0bc5431abaa02a4a211f7b1fd72cce1a55adb", size = 5090581, upload-time = "2026-06-28T18:41:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/24/d0/76653eb93c24ac8f4e80504ee27580a237ec6a8b78e34b55fe36c35597cb/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1dc993b410b3c48a5163ddc3c765b6ed6bffbaf37cb4b09df9221574b8e8648", size = 4785071, upload-time = "2026-06-28T18:41:50.208Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7e/637270b20db7c9dea87c1dfa7ed0590424a732071e28549491aed17bdc11/psqlpy-0.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f807f5cccb8ac0cf62647b848d795620e46645719162e8ade011810f09631632", size = 4951238, upload-time = "2026-06-28T18:41:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/08/d1/2bbde6600756bcc4611daa32ea237bca9d5ba6baa29990f859e8347df543/psqlpy-0.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9aeb02863f5ef024f14808439d9ed736af81bdb356ace5983f1cdbab53d8c2ef", size = 5141692, upload-time = "2026-06-28T18:41:53.763Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/425f2699ada907af2d7cd1dcea9df4f2e412dce00c016cc7c6d4c4d72c01/psqlpy-0.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb8016745770646b684f653c6028bab35b4ba1691bc25f23125b2f87f6afb2ac", size = 5250688, upload-time = "2026-06-28T18:41:55.662Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/bb5642854b58339e02a4116bf64eb5a96dca942ac0d7d81bcca6b5912a0e/psqlpy-0.12.1-cp312-cp312-win32.whl", hash = "sha256:6a30788d2d90ee00937eb0beba11d4e10d347313371a8071dede854a2f1ea595", size = 3640283, upload-time = "2026-06-28T18:41:57.243Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ab/f0c20d4c73f3b465cdb3f62d4db12a1ec6d478ee32c742247423f7d2a0d4/psqlpy-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:44bfa21da5d32fed963ec661733806aa61696e56950c029ce929b7e8dd89aeb0", size = 4299648, upload-time = "2026-06-28T18:41:58.558Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/c8b8277f9da3e22d903fda31a4bc40e4f31e2cbf9855aa700ff0815ee8d7/psqlpy-0.12.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5ceed709c46f138d402d7f5010e7ace4fcbd21e58fbc6176befac04881f05ee7", size = 4409400, upload-time = "2026-06-28T18:42:00.387Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/baeabd449ee9fb879076420b9a907d47785bba9aed37cc764473d5e8cb8d/psqlpy-0.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82e23042611be0d3a426be5099ee29025012218c2bc6a29a3ab0c1c3cbf27ea5", size = 4630017, upload-time = "2026-06-28T18:42:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/7c/03/81fa3630fa18950b038ab193f30840ef0469fa814693036286d847378fda/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:857d87490ab56578316ac09882c486438c75ffca2be9170bb6cf998b06c98028", size = 5172841, upload-time = "2026-06-28T18:42:03.368Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/806c18cb9fe28ade5f3c9da02f433647f35b55db0c5940e6a68a3e39857f/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3f6bc27927309987a0fc325ed2e7f7080f373a797d99db117e8d3193c851a3c", size = 4449849, upload-time = "2026-06-28T18:42:04.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/44/fe9d0bd25065305f36214cdb4c5eb007aafc0cefbe1ce5066a2085b2c781/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66356932d83ce011155cf5380b56965143273048a1bb49079655bcee083551df", size = 5084101, upload-time = "2026-06-28T18:42:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/3d/15/e9006cf4cff1bbd0101edb6372fdd5bfd1bb035e31598f861e15ae245e85/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec2a40e4d04181b7dcc50824c7a2f254add6399caf96d868ec1dc3d5af2d913f", size = 5090342, upload-time = "2026-06-28T18:42:08.069Z" }, + { url = "https://files.pythonhosted.org/packages/32/ff/89542c6630dde48edb6523d52aabe4d172c27c39ca605d2282a2c52afd0b/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec4dfb83c8e3893da51352954be387edc9fc330f938843c88ada0fe080cef0d4", size = 4788578, upload-time = "2026-06-28T18:42:09.537Z" }, + { url = "https://files.pythonhosted.org/packages/f5/17/2ea4cf620f75e8d3f8df85aa6e176f987a7371c594730d75d2c96463790e/psqlpy-0.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d270234cf63624c8c825bd3e94bc2ad77b6002123e1cd18ca2a474a2847191", size = 4951653, upload-time = "2026-06-28T18:42:10.967Z" }, + { url = "https://files.pythonhosted.org/packages/77/2e/c75c8f68ea16043cc3f41508a9df82253e4e5c8276621d03856474ca5474/psqlpy-0.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c4b3ae07fd5bd5abd52b5650ce960d9d715122bb15c834d83f8eefa7b8d80121", size = 5141236, upload-time = "2026-06-28T18:42:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/42/d8/34f1e422629231a7118e94d95827b92c63b00711fa7f09fd8976cd4ebdb3/psqlpy-0.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62a196493bf67d77aafd3c77413bbf9c7ed59c4620bafdf1e7d595d8052b71ac", size = 5250133, upload-time = "2026-06-28T18:42:13.856Z" }, + { url = "https://files.pythonhosted.org/packages/5a/58/10ff06f23fe6042d3bb8ac957fca86185be233bb6f24aa354be00789635f/psqlpy-0.12.1-cp313-cp313-win32.whl", hash = "sha256:b661a601a39da8250282da58d59617b513a40bb0cabd99ed61d78cd5935991fe", size = 3639847, upload-time = "2026-06-28T18:42:15.593Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/6c751754ec08c8facdc547d4421b8a9697a05e7ef6f4b55892c541d8053b/psqlpy-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:985ecd6ab3ab08fa4bfd82a9e8163f42b40dfaec6cb198db382d19581061ee18", size = 4299422, upload-time = "2026-06-28T18:42:17.401Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5b/ba794a0de3dd200e0fdc4c38097cd3fd32b623ba3f26b53b4a1889260db2/psqlpy-0.12.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4e4ab046e5b6d7f1b56bd2b5132299fbf3789066e21cb6809e2f282fc15e3b30", size = 4412074, upload-time = "2026-06-28T18:42:19.223Z" }, + { url = "https://files.pythonhosted.org/packages/21/99/26245ad49f97d9fc728a1a2afb1d3539a25d24277148ab0772a96ce536a0/psqlpy-0.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19e7dd88b269d9b70b2901e066b476a7c5cef3e5a3803dfb859ceef9465d9a48", size = 4632479, upload-time = "2026-06-28T18:42:20.961Z" }, + { url = "https://files.pythonhosted.org/packages/04/d3/933712b1fe3b1e380b52b16fffaf27fc1b246e34133cef151790c721207c/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d207124e116e07e4cbb411b19f8e7c2b3ae8b186befadf26303a177c4cca698", size = 5171335, upload-time = "2026-06-28T18:42:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/db356715ea66a7b01faf4231b376b2edac78f2c84cee6219fb60d0c6c660/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9eb47797fe93f277d02d0ad27bbeec25a583e9ebe9acabc340e8e6eb0269c6f2", size = 4444673, upload-time = "2026-06-28T18:42:24.222Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ab/43e36e95512eb2f1d2d38e6707dbadc0bd7ace495f611eb9f7bd26cba249/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e579dab3be2b0d09bc6605d4a22e55cbac37cf074c9178379699a21276edee8", size = 5084430, upload-time = "2026-06-28T18:42:26.491Z" }, + { url = "https://files.pythonhosted.org/packages/23/4b/ef807e98e9b18da5fc683045ee0caa04b8915f57f70108aa82fcad9acb91/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cc86932f5fbf139394039b0f8c043c59977982b76912e930ed91210112cc04d", size = 5089500, upload-time = "2026-06-28T18:42:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/8f/47/a61c3ca2df3f422193ed061c244dcdb5fdaa14fb55cf11d249ca2714c3d6/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a0d08af7002358adb7bac419f5fbb653560be6604aba99722378a549bcbf887", size = 4787029, upload-time = "2026-06-28T18:42:29.453Z" }, + { url = "https://files.pythonhosted.org/packages/98/bd/b9325b192fffabf94d907ec54be11d60d35b064a5376b84865858d9d852e/psqlpy-0.12.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f12b24a26b6ac274d00e82ebc3a21d1dcfedb14e08da57e54d04e62fcd2f05", size = 4948069, upload-time = "2026-06-28T18:42:30.939Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/958c9a6cd2214c0c485cb10670d804cc4461c7f6ddc071c58549286efdcc/psqlpy-0.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:025f0e8e2d7bf0bf1ef5801ff9a402728021048d1d13e2112ca8b0c6d32cf9ba", size = 5142019, upload-time = "2026-06-28T18:42:32.668Z" }, + { url = "https://files.pythonhosted.org/packages/61/dd/afb4580af80fde43502400f50bb64f6d9390a25b38d3812b173f3d274ad8/psqlpy-0.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:733251b378908aa6c5738079c3f053c0fcaaf542412abb312cd3b7cdda696e1d", size = 5247075, upload-time = "2026-06-28T18:42:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/57/94/51ce28393efcde16d6881acd01c4bd3ab57c99258243244581fbe62d1890/psqlpy-0.12.1-cp314-cp314-win32.whl", hash = "sha256:31a5d8bb8fa1a13b7da6edbc183754cc68fc47cb931bd9b96b9edecb77d98522", size = 3637418, upload-time = "2026-06-28T18:42:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/51/3a/3116e5d4b1c7cd5fb4825263e0764c431c4e73f0e3e1002aa288c5de6596/psqlpy-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f7337f373a1d41d1cbc5a53f32232afd5b66135c191357dcc1d6cf4cf7d0c17c", size = 4300529, upload-time = "2026-06-28T18:42:37.3Z" }, + { url = "https://files.pythonhosted.org/packages/26/44/f51ec4c450c470cd36fd637fc85adcded77b2a88781d3dd20489acac4e6c/psqlpy-0.12.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ea3ac9ce92078137d571740dd935a5ef7520d00dd8669750c3da3e020ba84b3", size = 4419212, upload-time = "2026-06-28T18:42:39.208Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ce/f66d0ac91a2e17bcba61e18edc89fed59f5333f95b494a799882615eda08/psqlpy-0.12.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0b3a318922674ab36e38378828de603f87360404d160912946363c2c4a5157b5", size = 4647437, upload-time = "2026-06-28T18:42:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/fcfbf42a1115c5bf5ddd6a868d12cc2b50c12201996baf3d7ff87c53d4be/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4228e9f4a0a36dc58d7728e44b835e9a376fc339e227758546d31de9f872e9c", size = 5161527, upload-time = "2026-06-28T18:42:42.352Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/027e544fa348032112a09b4e75cfd5a836b2936c46f47cc2be2a3e97a7c3/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7925ffdb717fccb806883490fb134c5ef14ffdf7d05b5d9daef63ed3230ea056", size = 4433056, upload-time = "2026-06-28T18:42:43.77Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4a/468214456e7a3d11f46945129a628e2da75afceb00128466341bf3f99362/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1f63af6205c489a6b19cf0d93b810852b265920cf2a919151f9acc64469a76", size = 5077093, upload-time = "2026-06-28T18:42:45.46Z" }, + { url = "https://files.pythonhosted.org/packages/88/7a/0269ef3bb609ddfe37bde0de5cfda556b5120043ba816fc92136646cd241/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1adf04bc13086e1d2ee96bb4ce0eab1121c02ff3e3a98c9d5a581834a0008b84", size = 5093081, upload-time = "2026-06-28T18:42:46.918Z" }, + { url = "https://files.pythonhosted.org/packages/78/f1/2c69c933daa112ba0654e234ac5dd32d21509ce283262e378449a6af7995/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a6a10509d56f489cfda5852de5cfb36878497886a8c978ef68034eec560599", size = 4776795, upload-time = "2026-06-28T18:42:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/1c/87/667cc20b6a82bb75fd55ef759ce3d7bec23edbd4bb4babfd5d29a7e1dcf5/psqlpy-0.12.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6dea4a70ba98940e803d8d2d1fa83cf1694549b3a9f2e7378506a37da3f3eb", size = 4936554, upload-time = "2026-06-28T18:42:50.113Z" }, + { url = "https://files.pythonhosted.org/packages/10/db/8ae30cf4b096c4362c615d312924a3e22f0f08bdb45af89ca0ffd3ba01d6/psqlpy-0.12.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d5d811b158848b84935722fa3f43fc77aeb6cedb8481c53bc7fd0f4158c3ce5f", size = 5140165, upload-time = "2026-06-28T18:42:51.763Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/7a6858e3fc84517ac3ae66d32bf5dfa23ef9cb73ed5210f6c886f69be103/psqlpy-0.12.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5e9234ce2a749feb407ac5cf862db4d0e6e67a4f442053be3afb76567e8d4e21", size = 5243563, upload-time = "2026-06-28T18:42:53.415Z" }, + { url = "https://files.pythonhosted.org/packages/59/89/2d9e57c6fcc4e0b252232068789ace003a64c7dd373c227deb42d945942c/psqlpy-0.12.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a98a3fe1486a1fa570afc4ef2311232ee0c97107a621d845e525d3836d4472d", size = 4419025, upload-time = "2026-06-28T18:42:54.829Z" }, + { url = "https://files.pythonhosted.org/packages/9e/25/ba379d09662d2ad6d4c7ec9ca7c9079f8283b3f8361749d95e35771da04b/psqlpy-0.12.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0937bddd455d2b9673218e4a6635d07f92acad651d34111564732a447abfb2b4", size = 4648682, upload-time = "2026-06-28T18:42:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/080191f8c5ccb4cf9a915e6bb13ca5e0ccb2148fe7aede516eea2a51f120/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2f90453d54685b7dac3a4a41a5270df1a0894fde8dec98c7e066221e49e213e", size = 5162613, upload-time = "2026-06-28T18:42:57.819Z" }, + { url = "https://files.pythonhosted.org/packages/f7/76/dc99df6f234f3adfb8d022ce4edd7cb2ea463db790142bc83c8be03bfaf0/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73366a5d3a7ab4420a07e2314a1265400090340490bac30cd561a5319438507b", size = 4433673, upload-time = "2026-06-28T18:42:59.869Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f0/23965396bf5c8159563fe55a6611cfe25a4843feefe6c3d0bb4d338681cb/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24aba98e149e6787b039354765ed02ee0fb84fe5e971f196c9d1a28a8f29e18e", size = 5077031, upload-time = "2026-06-28T18:43:01.414Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/91a6a6b017d3d9b00662dde8ca28f7da37ad8666be5896cfea977b762e8c/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:564d38f362007920dbcb47e00a008023a18b01ea65333a07923d1cb12fe2b94b", size = 5093437, upload-time = "2026-06-28T18:43:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ce/f268e6408849e6a78e72d5b85dc33d947af0c68482a853ac012d961ad3dd/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a6e7f7111b24bb27140264b23f3ead44a707ab161b3b99aa001c4500144949", size = 4777678, upload-time = "2026-06-28T18:43:04.705Z" }, + { url = "https://files.pythonhosted.org/packages/3b/06/d5048f5b81643474f3050d1837e9977a3563b63b98197d3bcfeec046b11b/psqlpy-0.12.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a16793299b87b79cb776fb5d801e440254322c14d32acaefcd0fbeb912834394", size = 4935808, upload-time = "2026-06-28T18:43:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/ad7263b8a5b7845983d6d35bf5f76808b6db5b8de3645eb0fc8940eac2c5/psqlpy-0.12.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8952c56a422271b5bf0403329ede113d2dd09dd94e2e3690c11ec321d1887aeb", size = 5140570, upload-time = "2026-06-28T18:43:07.708Z" }, + { url = "https://files.pythonhosted.org/packages/92/2c/68a496f3d36d9e25d003084c866cc41b2643550b877098b11e3bcdfa1d1f/psqlpy-0.12.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1e3f39ff8aed5d5245c04422fcbe2a3f73bc230870905d814faaf3e900d27f1e", size = 5243462, upload-time = "2026-06-28T18:43:09.139Z" }, ] [[package]] @@ -6703,11 +6697,11 @@ asyncio = [ [[package]] name = "sqlglot" -version = "30.11.0" +version = "30.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/de/1542b04dcac0f85706fb8ce1ce7d2abcc230cdf10ea9e3aa7345393e5a90/sqlglot-30.11.0.tar.gz", hash = "sha256:1a23c6e2adb41da61fda46b1848d2fa26341d447fc0f0cd5ca21160362100991", size = 5893125, upload-time = "2026-06-11T17:11:37.845Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ed/a6c45aec29353b6392ea34548c40af3ac6ffd6bc5572cf23b2ce250876fc/sqlglot-30.12.0.tar.gz", hash = "sha256:6b8369704662d4f654bc934cea4dd31c916c2a571b389210cb9e951a275e5fd9", size = 5905110, upload-time = "2026-06-26T14:09:40.408Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/86/53edf106e8cd3c883ccd0c6b470bf00ddf877a86e667665343b2d597329d/sqlglot-30.11.0-py3-none-any.whl", hash = "sha256:cffdee57d1f2f5472dc9f13087e618cf795841172b7d5ef78b63a051a52d2710", size = 698721, upload-time = "2026-06-11T17:11:35.737Z" }, + { url = "https://files.pythonhosted.org/packages/db/9e/82a390ecc85f066ff80affa01d195f744e3de60ad4d695b8de31c9a66da3/sqlglot-30.12.0-py3-none-any.whl", hash = "sha256:86cccc610073c645c03e72b55b60ae0518aa3253a7fc3bd56551370d003c6554", size = 707583, upload-time = "2026-06-26T14:09:38.525Z" }, ] [package.optional-dependencies] @@ -6717,33 +6711,33 @@ c = [ [[package]] name = "sqlglotc" -version = "30.11.0" +version = "30.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sqlglot" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/af/2e872cabb05e081cc9a642dab24dbd8f49648e239e86b0b4fdb34dc0c588/sqlglotc-30.11.0.tar.gz", hash = "sha256:6ce71f4d31459df966f3752848f397e254ff5d4e2772594e539e23cff0f2bbc8", size = 485462, upload-time = "2026-06-11T17:10:41.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/dd/25e1fa80af3d8743f3abb004bde0b0abb7a1e90fa6669b87538148eee909/sqlglotc-30.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:742c108c8089d39280c229eb7a57838fc907f3b01620de948502b2f3211cf301", size = 31827077, upload-time = "2026-06-11T17:09:53.157Z" }, - { url = "https://files.pythonhosted.org/packages/7d/6d/c1295553871a6a0604eebfd836b212df763efeb9906e61701d0b2a523de4/sqlglotc-30.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76734c1684663fa2899a990446562c635c7057cc431bc1c79920e26423727488", size = 24311738, upload-time = "2026-06-11T17:09:56.031Z" }, - { url = "https://files.pythonhosted.org/packages/d5/00/3616f2a410d8db722fcf9130a652f72b2badc23eaaf7dcbb859057a55ebf/sqlglotc-30.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44eb032a8ebd50bd7d835024f5f50363e1720fbbed1710bca8160f26ebeab59b", size = 25425637, upload-time = "2026-06-11T17:09:58.483Z" }, - { url = "https://files.pythonhosted.org/packages/f9/2e/436807b5ece023ee0d8e7bf447fd5fed56e6e6deeed6d466025d067fa2e7/sqlglotc-30.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fb6b4aa751485d5415539762ff16668978436eff946de51985d811c23aba1d4", size = 10659714, upload-time = "2026-06-11T17:10:01.109Z" }, - { url = "https://files.pythonhosted.org/packages/21/57/ddd3cf0827942c3a76b0572c3a19311d3d5848eedb1a6ad6403b72534efb/sqlglotc-30.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:349e6b1e79baefebeda9ea457bb06a8f348aaacf0fc39c2f4b6a54ddef19cbb4", size = 31663756, upload-time = "2026-06-11T17:10:03.295Z" }, - { url = "https://files.pythonhosted.org/packages/58/66/94a30711818273dd1534141c2913e1462cb7e592878b53b6b4e73ef2dc3c/sqlglotc-30.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eedab4e58c2baa2bf6b9864505aaf7bdbde92c0370854dfdbce5a3cf72bc4be4", size = 24514156, upload-time = "2026-06-11T17:10:05.76Z" }, - { url = "https://files.pythonhosted.org/packages/d0/5a/062a85642f23b30a5aebe075a6d52d2bcfee020d190e9b171c85b9516447/sqlglotc-30.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5759b32f0492f5196f7611f9f5233ff636e91441d9b5cd2309c939cd532f11c", size = 25668183, upload-time = "2026-06-11T17:10:08.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/2a/e4c1b9e19d689ccb65cfeba089e495204ca74560f4cd3afd81b83d7fb421/sqlglotc-30.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:64f0a3d05a92aef5911a2aed058af3195ea08a11a68b3d7befe6dd04d3c2aa66", size = 10651113, upload-time = "2026-06-11T17:10:10.792Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6b/a03065644831f7f761442278337516fa5769b9a29d4843e88720a36edf80/sqlglotc-30.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2f6741bff8c92777ecdd05f97d5dcd61614c05b1a01dbf7b10198dfd176060d6", size = 31867715, upload-time = "2026-06-11T17:10:13.124Z" }, - { url = "https://files.pythonhosted.org/packages/ea/3e/85d4928a997657010c440eabba2dc11b43dfde8217c06ba203cf4b27f510/sqlglotc-30.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff9d1917f5238e6d8261c79bc911726787220fa85805beca348a36d4aecc71f8", size = 25398346, upload-time = "2026-06-11T17:10:15.512Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/5e29f763bebaf09b43cbd4470eaa8077f128dad4ac6444ed76533a235b88/sqlglotc-30.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba3a0f215ba81c276759a4e6331f8af1948d803d38c3f5ed684119c5d6f1d82a", size = 26607451, upload-time = "2026-06-11T17:10:17.871Z" }, - { url = "https://files.pythonhosted.org/packages/3e/50/b1d10a0b3da4e327482c1f5bb92d1ed2f9a4fe149678d15805147663a933/sqlglotc-30.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:9c1cdfc5544cc4e23d31b966f007cc2be16e697cef86b97cba2ffd246f344256", size = 10863406, upload-time = "2026-06-11T17:10:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/83/96/ba4314282848c7687e0c5c89fe8190308b1855d0c6d6b361f8e5582abce2/sqlglotc-30.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c36388963d82aacb4287c7859cc40a08c15fa0dfaf0ad055679b53ba602016b", size = 31694909, upload-time = "2026-06-11T17:10:22.137Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/5420bfbc518da22b256a178725ce48b068ce4c12d7521dc4fb5a80c99783/sqlglotc-30.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a357a6bf6321796c8506520340feb5052bf76c658a2455aabbbdd7d85c8fd5da", size = 24983310, upload-time = "2026-06-11T17:10:24.619Z" }, - { url = "https://files.pythonhosted.org/packages/0f/90/351fa5cd0c0cdb20bec31b399b56536c5935233c46c894ad42ebfc7cc95b/sqlglotc-30.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba16b178d2ad9ab1eac38fb1415434abecaf342d3a487c3128649fe8c0d1e500", size = 26236612, upload-time = "2026-06-11T17:10:26.918Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/76653f81b027746a8f21d70ec33e7315aae29363184b4b780c5b57458b57/sqlglotc-30.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:20e662593a96c0086edc46b336bc772d213901fd09add99a3326661793ba7018", size = 10851707, upload-time = "2026-06-11T17:10:29.259Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f2/1d742df787a61d09fe93b9e454a8fa88ca2d73a8a5c8f9a3ad45c7993e39/sqlglotc-30.11.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d49533b4a851f3603ecba9a724c32f227bca90cc0034b8a3ad66acdbde759a90", size = 31599156, upload-time = "2026-06-11T17:10:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/fb/28/81215d24dc14196f6b9f106de6522d871b0bc49666dc25b680b353b6ea11/sqlglotc-30.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2cbeec4ec4591268585e5ad5f55fbee30ccd68227b5d2f77d483cdd63b65963", size = 24982016, upload-time = "2026-06-11T17:10:34.68Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4e/528b0d8d8f18edfe2b210983bf81234586b4108220d59c1fcb132bd9b594/sqlglotc-30.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f4bcc7b76634171edccc13aabba9c1bea51f97caf2187daacbe433004058dc3", size = 26140612, upload-time = "2026-06-11T17:10:37.406Z" }, - { url = "https://files.pythonhosted.org/packages/18/e5/d52b6d4e8e06c1a9771eff5886da6a4381aa624d94b5a8cc846f22a2b270/sqlglotc-30.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:8c6ed84d557772880a082ef29d18b0c2cfee2d177e6528b30df2b7eaf6adb585", size = 10984419, upload-time = "2026-06-11T17:10:39.797Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/36/05/bdb3c3433f19324ed3499bc51adb6db934f77d947ed1c624554523b22966/sqlglotc-30.12.0.tar.gz", hash = "sha256:7c4c9c7d76026b75f64a6682faf84cf5145e3304190c07807b86962d8d535f74", size = 491114, upload-time = "2026-06-26T14:08:47.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/69af8ad60cfbef116a2f63bf5d455bdcaafb369fa27772b4936051e76063/sqlglotc-30.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1e5ad6689bf76f226e4a3182f3819e9e4056ea0d2b7c1fd610dad22e065b7a10", size = 32008565, upload-time = "2026-06-26T14:08:02.422Z" }, + { url = "https://files.pythonhosted.org/packages/7e/93/bc5cf48a0fcbc5967687a4f2dfaedf7d2dfd03ae0bca0a57d95e7cbf6495/sqlglotc-30.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8412ddb9bb2e3832e36a9d2c279a2c64ffa0d695295f2daae72cef4f68114cb0", size = 24435127, upload-time = "2026-06-26T14:08:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/07/51/6db6cf0fc07ee398995582dd367fba286be419c06d27c51bf941bc9d09a0/sqlglotc-30.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30fe29c8287dc681b8e6c1514c4138861230314ebe86a47081cf4d1141dd2f89", size = 25549128, upload-time = "2026-06-26T14:08:07.303Z" }, + { url = "https://files.pythonhosted.org/packages/92/a4/74764a4cb764d8acfd4e85127724ce3ca22a663c34cebe3cfd8f4a864224/sqlglotc-30.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:671d7fcb7261b297c014f7cdc54703e28d3de6e58e94a5652beb5a661a3d0786", size = 10735833, upload-time = "2026-06-26T14:08:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/98/00/82c2bc0c49aacdfe53006ebd75d00e9cca536b3d0fe5c661d4c1d36ca27c/sqlglotc-30.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:05f0cf61aba3207aa8d9fe10646168eac01595f521f3d81390ba215a15e8f6b7", size = 31721081, upload-time = "2026-06-26T14:08:11.587Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/dac3d917d445301af839bd6ab165edac2378f9d6f0ef583d6e53cd1acd46/sqlglotc-30.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71784e43ef4af200a3e8e04f42b9977f405cfeee025c7dbeb703b862029b2b3f", size = 24638763, upload-time = "2026-06-26T14:08:14.062Z" }, + { url = "https://files.pythonhosted.org/packages/40/67/c4c8258fc706dd8b349e39fb76b7f26ed3478f6fbbdfe688f341d68c981e/sqlglotc-30.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a84dd9d7565b782bef07e577b3400cdcddc81ffaae963d5e8460e431c4e70e27", size = 25798453, upload-time = "2026-06-26T14:08:16.347Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/dc0265b1c7abab70d1011b2390dcace13a157b2fa3b1fc753be0144a9566/sqlglotc-30.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:4588ea73e99ec2f106cd688e6c12199b99c509497ec16fa858b645f748f461d0", size = 10729513, upload-time = "2026-06-26T14:08:18.477Z" }, + { url = "https://files.pythonhosted.org/packages/70/b2/246dba0b5ca294ba6c8d087e7b6bbd56c94e17298ff14a0232af70ba086a/sqlglotc-30.12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c4016010ecf0b3473f0b2004f9bf352b9c694644a18867e290cf1ddfcd994a1", size = 32052780, upload-time = "2026-06-26T14:08:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/7ead345c4a6395a1eef166db6589e32082a8184da6ba8042b115015449c6/sqlglotc-30.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddad6b33e98f040aa2493e6187e83235c57a4d52ba573a2f94c32d9209b3e7e0", size = 25534639, upload-time = "2026-06-26T14:08:22.499Z" }, + { url = "https://files.pythonhosted.org/packages/fb/91/7e596f8be4116adc0b9911fc87c214a0ebea55fffac1ca7068483ef0fb07/sqlglotc-30.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d998e3b5eddf5db65573009f86cb6a0378efb7e1780d19a6357bce67afb05f0d", size = 26748971, upload-time = "2026-06-26T14:08:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/6e9a29b08a51f2fe8d3774bc1d051e8232375557fa57cbc9b7b6f000d53b/sqlglotc-30.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:1075dbee8fde16aa372ac42a63941cde50d533e57bbe0a34ddc6ab8c328dbf45", size = 10945978, upload-time = "2026-06-26T14:08:27.377Z" }, + { url = "https://files.pythonhosted.org/packages/90/a3/1e89af7986c5ffd58809ddb756c0a3d85129658e10cb023b1cf7aa61ac3e/sqlglotc-30.12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cad61b20aa2e8ed01efca83d6fa475d14214bcb1caba5eb11ab3b5b6fdd50ba5", size = 31880909, upload-time = "2026-06-26T14:08:29.476Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d6/a99f862c66e5662bf20fb77d62012cba2f2dfbd0f9d63c97fe30468fd3ff/sqlglotc-30.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb8836127f5233606bc63c2f56dc529e5ee9c96b1e317f434c65d90984fcdad1", size = 25120994, upload-time = "2026-06-26T14:08:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7e359fa2e6197d1c6566175428cf1af029602858b477fd07ea0986dae236/sqlglotc-30.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1153b757420fc712163bb8da12a14f2603bcd6cdd40b0aedd526e16c68f56b9c", size = 26371919, upload-time = "2026-06-26T14:08:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/37/b4/5774537e439018c42618b2084fb0a127d62687153b0b2b6893d0d39e17f0/sqlglotc-30.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:2b7454ebbd97d3a197d946fa352ce64f58d6bdacac27883929fd9d45900618f8", size = 10933673, upload-time = "2026-06-26T14:08:36.374Z" }, + { url = "https://files.pythonhosted.org/packages/79/21/762087c42e56c126ab20c98cb029ef895a666ab7f77af1ca144e0333225c/sqlglotc-30.12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1ede716d4bc7bda5558354deb545dda7400ae475c543af08bbb46fc2bab99aae", size = 31785229, upload-time = "2026-06-26T14:08:38.703Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/70f14f11fd5e5f3be654529909dcb2036baddd95fbaa3b221276d13a53a7/sqlglotc-30.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cdb2e5f7416daaa5f3046cc790e2ab31ddbde930443ecfe548cfd1e5f9552e7", size = 25117316, upload-time = "2026-06-26T14:08:41.004Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/6cf38b0203f9c81588aca6be4e40679acf8f2087ca4fee7cc67f4af15a71/sqlglotc-30.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0756aff3d6617d8587027981b15a57b27a698f3082ed7d333d313fed0e59de5d", size = 26272124, upload-time = "2026-06-26T14:08:43.104Z" }, + { url = "https://files.pythonhosted.org/packages/f8/bd/efe78e48b97593e01ceb91090aeb9dfb69e470c6f2dcd7fc5f0f0b637e46/sqlglotc-30.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:efcf65f06d5de8b594cb7bda5dbc2e2b29e06eaef1d90044778751fed455b1dd", size = 11065847, upload-time = "2026-06-26T14:08:45.229Z" }, ] [[package]] @@ -6858,7 +6852,7 @@ orjson = [ ] pandas = [ { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyarrow" }, ] performance = [