From 304b3c2969f731aae7e2be4d0d34ac168a81a9b8 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Mon, 25 May 2026 17:00:59 +0200 Subject: [PATCH 1/6] feat(databricks): add Databricks SQL adapter Three-level Unity Catalog (catalog.schema.table) via databricks-sql-connector. Supports PAT, OAuth U2M (browser), and OAuth M2M (service principal) auth. --- README.md | 3 +- pyproject.toml | 6 + .../providers/databricks/__init__.py | 1 + .../providers/databricks/adapter.py | 252 ++++++++++++++++++ .../providers/databricks/provider.py | 29 ++ .../providers/databricks/schema.py | 91 +++++++ tests/unit/test_databricks_adapter.py | 228 ++++++++++++++++ 7 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 sqlit/domains/connections/providers/databricks/__init__.py create mode 100644 sqlit/domains/connections/providers/databricks/adapter.py create mode 100644 sqlit/domains/connections/providers/databricks/provider.py create mode 100644 sqlit/domains/connections/providers/databricks/schema.py create mode 100644 tests/unit/test_databricks_adapter.py diff --git a/README.md b/README.md index 8ca45e6b..cceeb0aa 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ --- ### Connect -Supports all major databases: SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, FirebirdSQL, Oracle, DuckDB, CockroachDB, ClickHouse, Snowflake, Supabase, CloudFlare D1, Turso, Athena, BigQuery, Spanner, RedShift, IBM Db2, SAP HANA, Teradata, Trino, Presto, Apache Flight SQL, Apache Impala, SurrealDB and osquery. +Supports all major databases: SQL Server, PostgreSQL, MySQL, SQLite, MariaDB, FirebirdSQL, Oracle, DuckDB, CockroachDB, ClickHouse, Snowflake, Databricks, Supabase, CloudFlare D1, Turso, Athena, BigQuery, Spanner, RedShift, IBM Db2, SAP HANA, Teradata, Trino, Presto, Apache Flight SQL, Apache Impala, SurrealDB and osquery. ![Database Providers](docs/demos/demo-providers.gif) @@ -289,6 +289,7 @@ Most of the time you can just run `sqlit` and connect. If a Python driver is mis | Turso | `libsql` | `pipx inject sqlit-tui libsql` | `python -m pip install libsql` | | Cloudflare D1 | `requests` | `pipx inject sqlit-tui requests` | `python -m pip install requests` | | Snowflake | `snowflake-connector-python` | `pipx inject sqlit-tui snowflake-connector-python` | `python -m pip install snowflake-connector-python` | +| Databricks | `databricks-sql-connector` | `pipx inject sqlit-tui databricks-sql-connector` | `python -m pip install databricks-sql-connector` | | Firebird | `firebirdsql` | `pipx inject sqlit-tui firebirdsql` | `python -m pip install firebirdsql` | | Athena | `pyathena` | `pipx inject sqlit-tui pyathena` | `python -m pip install pyathena` | | BigQuery | `google-cloud-bigquery` | `pipx inject sqlit-tui google-cloud-bigquery` | `python -m pip install google-cloud-bigquery` | diff --git a/pyproject.toml b/pyproject.toml index 7940b1ef..3d894366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ all = [ "impyla>=0.18.0", "osquery>=3.0.0", "surrealdb>=1.0.0", + "databricks-sql-connector>=3.0.0", ] postgres = ["psycopg2-binary>=2.9.0"] cockroachdb = ["psycopg2-binary>=2.9.0"] @@ -88,6 +89,7 @@ flight = ["adbc-driver-flightsql>=1.0.0"] impala = ["impyla>=0.18.0"] osquery = ["osquery>=3.0.0"] surrealdb = ["surrealdb>=1.0.0"] +databricks = ["databricks-sql-connector>=3.0.0"] ssh = [ "sshtunnel>=0.4.0", "paramiko>=2.0.0,<4.0.0", @@ -253,6 +255,10 @@ module = [ "impala.dbapi", "osquery", "surrealdb", + "databricks", + "databricks.sql", + "databricks.sdk", + "databricks.sdk.core", "google.cloud", "google.cloud.bigquery", "google.cloud.bigquery.dbapi", diff --git a/sqlit/domains/connections/providers/databricks/__init__.py b/sqlit/domains/connections/providers/databricks/__init__.py new file mode 100644 index 00000000..3bbc4837 --- /dev/null +++ b/sqlit/domains/connections/providers/databricks/__init__.py @@ -0,0 +1 @@ +"""Provider package.""" diff --git a/sqlit/domains/connections/providers/databricks/adapter.py b/sqlit/domains/connections/providers/databricks/adapter.py new file mode 100644 index 00000000..2332a797 --- /dev/null +++ b/sqlit/domains/connections/providers/databricks/adapter.py @@ -0,0 +1,252 @@ +"""Databricks adapter using databricks-sql-connector. + +Databricks SQL uses a three-level namespace via Unity Catalog: + catalog.schema.table + +We map Databricks' "catalog" to the generic `database` slot in +sqlit's connection model, mirroring how Trino is handled. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from sqlit.domains.connections.providers.adapters.base import ( + ColumnInfo, + CursorBasedAdapter, + IndexInfo, + SequenceInfo, + TableInfo, + TriggerInfo, +) + +if TYPE_CHECKING: + from sqlit.domains.connections.domain.config import ConnectionConfig + + +class DatabricksAdapter(CursorBasedAdapter): + """Adapter for Databricks SQL warehouses and clusters.""" + + @property + def name(self) -> str: + return "Databricks" + + @property + def install_extra(self) -> str: + return "databricks" + + @property + def install_package(self) -> str: + return "databricks-sql-connector" + + @property + def driver_import_names(self) -> tuple[str, ...]: + return ("databricks.sql",) + + @property + def supports_multiple_databases(self) -> bool: + return True + + @property + def supports_cross_database_queries(self) -> bool: + return True + + @property + def supports_stored_procedures(self) -> bool: + return False + + @property + def supports_indexes(self) -> bool: + return False + + @property + def supports_triggers(self) -> bool: + return False + + @property + def supports_sequences(self) -> bool: + return False + + @property + def default_schema(self) -> str: + return "default" + + @property + def system_databases(self) -> frozenset[str]: + # Built-in Databricks catalogs we usually want to hide from the + # primary picker. `samples` is the public demo catalog and + # `system` holds Unity Catalog telemetry. + return frozenset({"system"}) + + def apply_database_override(self, config: ConnectionConfig, database: str) -> ConnectionConfig: + """Apply a default catalog for unqualified queries.""" + if not database: + return config + return config.with_endpoint(database=database) + + def connect(self, config: ConnectionConfig) -> Any: + sql_module = self._import_driver_module( + "databricks.sql", + driver_name=self.name, + extra_name=self.install_extra, + package_name=self.install_package, + ) + + endpoint = config.tcp_endpoint + if endpoint is None: + raise ValueError("Databricks connections require a TCP-style endpoint.") + + extras = config.options + http_path = extras.get("http_path") or config.extra_options.get("http_path") + if not http_path: + raise ValueError("Databricks requires an HTTP Path (SQL warehouse or cluster).") + + connect_args: dict[str, Any] = { + "server_hostname": endpoint.host, + "http_path": http_path, + } + + catalog = endpoint.database + if catalog: + connect_args["catalog"] = catalog + schema = extras.get("schema") + if schema: + connect_args["schema"] = schema + + auth_type = extras.get("auth_type", "pat") + if auth_type == "pat": + token = extras.get("access_token") or endpoint.password + if not token: + raise ValueError("Databricks PAT authentication requires an access token.") + connect_args["access_token"] = token + elif auth_type == "oauth-u2m": + connect_args["auth_type"] = "databricks-oauth" + elif auth_type == "oauth-m2m": + client_id = extras.get("client_id") + client_secret = extras.get("client_secret") + if not client_id or not client_secret: + raise ValueError( + "Databricks OAuth (Service Principal) requires client_id and client_secret." + ) + connect_args["credentials_provider"] = _build_m2m_credentials_provider( + endpoint.host, client_id, client_secret + ) + else: + raise ValueError(f"Unknown Databricks auth_type: {auth_type}") + + connect_args.update(config.extra_options) + # http_path may have been passed via extra_options; drop the legacy key + # so it isn't sent twice if both schemes were used. + connect_args.pop("http_path", None) + connect_args["http_path"] = http_path + + return sql_module.connect(**connect_args) + + def get_databases(self, conn: Any) -> list[str]: + """List Unity Catalog catalogs.""" + cursor = conn.cursor() + # SHOW CATALOGS is universally supported and avoids needing + # SELECT privilege on system.information_schema. + cursor.execute("SHOW CATALOGS") + return [row[0] for row in cursor.fetchall()] + + def get_tables(self, conn: Any, database: str | None = None) -> list[TableInfo]: + cursor = conn.cursor() + if database: + cursor.execute( + "SELECT table_schema, table_name FROM " + f"{self.quote_identifier(database)}.information_schema.tables " + "WHERE table_type IN ('MANAGED', 'EXTERNAL', 'BASE TABLE') " + "ORDER BY table_schema, table_name" + ) + return [(row[0], row[1]) for row in cursor.fetchall()] + + cursor.execute("SHOW TABLES") + return [(row[0], row[1]) for row in cursor.fetchall()] + + def get_views(self, conn: Any, database: str | None = None) -> list[TableInfo]: + cursor = conn.cursor() + if database: + cursor.execute( + "SELECT table_schema, table_name FROM " + f"{self.quote_identifier(database)}.information_schema.views " + "ORDER BY table_schema, table_name" + ) + return [(row[0], row[1]) for row in cursor.fetchall()] + + cursor.execute("SHOW VIEWS") + # SHOW VIEWS columns: database, viewName, isTemporary + return [(row[0], row[1]) for row in cursor.fetchall()] + + def get_columns( + self, conn: Any, table: str, database: str | None = None, schema: str | None = None + ) -> list[ColumnInfo]: + cursor = conn.cursor() + schema_name = schema or self.default_schema + if database: + cursor.execute( + "SELECT column_name, data_type FROM " + f"{self.quote_identifier(database)}.information_schema.columns " + "WHERE table_schema = ? AND table_name = ? " + "ORDER BY ordinal_position", + (schema_name, table), + ) + else: + cursor.execute( + "SELECT column_name, data_type FROM information_schema.columns " + "WHERE table_schema = ? AND table_name = ? " + "ORDER BY ordinal_position", + (schema_name, table), + ) + return [ColumnInfo(name=row[0], data_type=row[1]) for row in cursor.fetchall()] + + def get_procedures(self, conn: Any, database: str | None = None) -> list[str]: + return [] + + def get_indexes(self, conn: Any, database: str | None = None) -> list[IndexInfo]: + return [] + + def get_triggers(self, conn: Any, database: str | None = None) -> list[TriggerInfo]: + return [] + + def get_sequences(self, conn: Any, database: str | None = None) -> list[SequenceInfo]: + return [] + + def quote_identifier(self, name: str) -> str: + """Quote identifier using backticks (Databricks/Spark SQL standard).""" + escaped = name.replace("`", "``") + return f"`{escaped}`" + + def build_select_query( + self, table: str, limit: int, database: str | None = None, schema: str | None = None + ) -> str: + schema_name = schema or self.default_schema + if database and schema_name: + return ( + f"SELECT * FROM {self.quote_identifier(database)}." + f"{self.quote_identifier(schema_name)}." + f"{self.quote_identifier(table)} LIMIT {limit}" + ) + if schema_name: + return f"SELECT * FROM {self.quote_identifier(schema_name)}.{self.quote_identifier(table)} LIMIT {limit}" + return f"SELECT * FROM {self.quote_identifier(table)} LIMIT {limit}" + + +def _build_m2m_credentials_provider(host: str, client_id: str, client_secret: str) -> Any: + """Return a credentials_provider callable for Databricks OAuth M2M. + + Imported lazily so the databricks-sdk dependency is only required + when the user actually selects service-principal auth. + """ + + def _factory() -> Any: + from databricks.sdk.core import Config, oauth_service_principal + + cfg = Config( + host=host if host.startswith(("http://", "https://")) else f"https://{host}", + client_id=client_id, + client_secret=client_secret, + ) + return oauth_service_principal(cfg) + + return _factory diff --git a/sqlit/domains/connections/providers/databricks/provider.py b/sqlit/domains/connections/providers/databricks/provider.py new file mode 100644 index 00000000..05cff445 --- /dev/null +++ b/sqlit/domains/connections/providers/databricks/provider.py @@ -0,0 +1,29 @@ +"""Provider registration.""" + +from sqlit.domains.connections.providers.adapter_provider import build_adapter_provider +from sqlit.domains.connections.providers.catalog import register_provider +from sqlit.domains.connections.providers.databricks.schema import SCHEMA +from sqlit.domains.connections.providers.model import DatabaseProvider, ProviderSpec + + +def _provider_factory(spec: ProviderSpec) -> DatabaseProvider: + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + return build_adapter_provider(spec, SCHEMA, DatabricksAdapter()) + + +SPEC = ProviderSpec( + db_type="databricks", + display_name="Databricks", + schema_path=("sqlit.domains.connections.providers.databricks.schema", "SCHEMA"), + supports_ssh=False, + is_file_based=False, + has_advanced_auth=True, + default_port="", + requires_auth=True, + badge_label="DBRX", + url_schemes=("databricks",), + provider_factory=_provider_factory, +) + +register_provider(SPEC) diff --git a/sqlit/domains/connections/providers/databricks/schema.py b/sqlit/domains/connections/providers/databricks/schema.py new file mode 100644 index 00000000..413acda0 --- /dev/null +++ b/sqlit/domains/connections/providers/databricks/schema.py @@ -0,0 +1,91 @@ +"""Connection schema for Databricks SQL.""" + +from sqlit.domains.connections.providers.schema_helpers import ( + ConnectionSchema, + FieldType, + SchemaField, + SelectOption, +) + + +def _get_databricks_auth_options() -> tuple[SelectOption, ...]: + return ( + SelectOption("pat", "Personal Access Token"), + SelectOption("oauth-u2m", "OAuth (Browser)"), + SelectOption("oauth-m2m", "OAuth (Service Principal)"), + ) + + +_AUTH_NEEDS_TOKEN = {"pat"} +_AUTH_NEEDS_SP = {"oauth-m2m"} + + +SCHEMA = ConnectionSchema( + db_type="databricks", + display_name="Databricks", + fields=( + SchemaField( + name="server", + label="Server Hostname", + placeholder="dbc-a1b2cd34-e5f6.cloud.databricks.com", + required=True, + description="Databricks SQL warehouse server hostname (no protocol)", + ), + SchemaField( + name="http_path", + label="HTTP Path", + placeholder="/sql/1.0/warehouses/abcdef1234567890", + required=True, + description="HTTP path of the SQL warehouse or cluster", + ), + SchemaField( + name="auth_type", + label="Authentication", + field_type=FieldType.DROPDOWN, + options=_get_databricks_auth_options(), + default="pat", + ), + SchemaField( + name="access_token", + label="Access Token", + field_type=FieldType.PASSWORD, + placeholder="dapi...", + group="credentials", + description="Personal Access Token (PAT)", + visible_when=lambda v: v.get("auth_type", "pat") in _AUTH_NEEDS_TOKEN, + ), + SchemaField( + name="client_id", + label="Client ID", + placeholder="service-principal-client-id", + required=False, + group="credentials", + visible_when=lambda v: v.get("auth_type") in _AUTH_NEEDS_SP, + ), + SchemaField( + name="client_secret", + label="Client Secret", + field_type=FieldType.PASSWORD, + placeholder="(secret)", + required=False, + group="credentials", + visible_when=lambda v: v.get("auth_type") in _AUTH_NEEDS_SP, + ), + SchemaField( + name="database", + label="Catalog", + placeholder="main", + required=False, + description="Unity Catalog name (top-level namespace)", + ), + SchemaField( + name="schema", + label="Schema", + placeholder="default", + required=False, + description="Default schema within the catalog", + ), + ), + supports_ssh=False, + has_advanced_auth=True, +) diff --git a/tests/unit/test_databricks_adapter.py b/tests/unit/test_databricks_adapter.py new file mode 100644 index 00000000..28420a26 --- /dev/null +++ b/tests/unit/test_databricks_adapter.py @@ -0,0 +1,228 @@ +"""Unit tests for Databricks adapter.""" + +from __future__ import annotations + +import sys +import types +from unittest.mock import MagicMock, patch + +import pytest + +from tests.helpers import ConnectionConfig + + +def _fake_databricks_sql() -> tuple[MagicMock, dict[str, types.ModuleType]]: + """Build a fake `databricks.sql` module hierarchy.""" + databricks_pkg = types.ModuleType("databricks") + databricks_sql = types.ModuleType("databricks.sql") + connect = MagicMock(name="databricks.sql.connect") + databricks_sql.connect = connect # type: ignore[attr-defined] + databricks_pkg.sql = databricks_sql # type: ignore[attr-defined] + modules = {"databricks": databricks_pkg, "databricks.sql": databricks_sql} + return connect, modules + + +class TestDatabricksAdapter: + def test_connect_pat_default(self): + connect, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig( + name="test", + db_type="databricks", + server="dbc-xyz.cloud.databricks.com", + database="main", + options={ + "http_path": "/sql/1.0/warehouses/abcdef", + "auth_type": "pat", + "access_token": "dapi-xxx", + "schema": "default", + }, + ) + adapter.connect(config) + connect.assert_called_once_with( + server_hostname="dbc-xyz.cloud.databricks.com", + http_path="/sql/1.0/warehouses/abcdef", + catalog="main", + schema="default", + access_token="dapi-xxx", + ) + + def test_connect_pat_token_from_password_field(self): + """If access_token isn't in options, the endpoint password is used.""" + connect, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig( + name="test", + db_type="databricks", + server="host", + password="legacy-pat", + options={"http_path": "/sql/1.0/warehouses/x"}, + ) + adapter.connect(config) + args = connect.call_args.kwargs + assert args["access_token"] == "legacy-pat" + + def test_connect_oauth_u2m(self): + connect, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig( + name="test", + db_type="databricks", + server="host", + options={ + "http_path": "/sql/1.0/warehouses/x", + "auth_type": "oauth-u2m", + }, + ) + adapter.connect(config) + args = connect.call_args.kwargs + assert args["auth_type"] == "databricks-oauth" + assert "access_token" not in args + + def test_connect_missing_http_path_raises(self): + _, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig(name="t", db_type="databricks", server="host", password="x") + with pytest.raises(ValueError, match="HTTP Path"): + adapter.connect(config) + + def test_connect_missing_pat_raises(self): + _, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig( + name="t", + db_type="databricks", + server="host", + options={"http_path": "/sql/1.0/warehouses/x", "auth_type": "pat"}, + ) + with pytest.raises(ValueError, match="access token"): + adapter.connect(config) + + def test_connect_m2m_requires_client_credentials(self): + _, modules = _fake_databricks_sql() + with patch.dict(sys.modules, modules): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + config = ConnectionConfig( + name="t", + db_type="databricks", + server="host", + options={"http_path": "/sql/1.0/warehouses/x", "auth_type": "oauth-m2m"}, + ) + with pytest.raises(ValueError, match="client_id"): + adapter.connect(config) + + def test_get_databases_uses_show_catalogs(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [("main",), ("samples",), ("hive_metastore",)] + + result = adapter.get_databases(mock_conn) + + mock_cursor.execute.assert_called_with("SHOW CATALOGS") + assert result == ["main", "samples", "hive_metastore"] + + def test_get_tables_with_catalog_uses_info_schema(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [ + ("default", "trips"), + ("analytics", "events"), + ] + + tables = adapter.get_tables(mock_conn, database="main") + sql = mock_cursor.execute.call_args[0][0] + assert "`main`.information_schema.tables" in sql + assert tables == [("default", "trips"), ("analytics", "events")] + + def test_get_columns_uses_info_schema(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [ + ("id", "BIGINT"), + ("name", "STRING"), + ] + + cols = adapter.get_columns( + mock_conn, "trips", database="main", schema="default" + ) + + args = mock_cursor.execute.call_args[0] + assert "`main`.information_schema.columns" in args[0] + assert args[1] == ("default", "trips") + assert [c.name for c in cols] == ["id", "name"] + assert cols[0].data_type == "BIGINT" + + def test_quote_identifier_uses_backticks(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + assert adapter.quote_identifier("foo") == "`foo`" + assert adapter.quote_identifier("a`b") == "`a``b`" + + def test_build_select_query(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + assert ( + adapter.build_select_query("trips", 10, database="main", schema="default") + == "SELECT * FROM `main`.`default`.`trips` LIMIT 10" + ) + assert ( + adapter.build_select_query("trips", 10, schema="default") + == "SELECT * FROM `default`.`trips` LIMIT 10" + ) + + def test_capabilities(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + assert adapter.supports_multiple_databases is True + assert adapter.supports_cross_database_queries is True + assert adapter.supports_stored_procedures is False + assert adapter.supports_indexes is False + assert adapter.supports_triggers is False + assert adapter.supports_sequences is False + assert adapter.default_schema == "default" + + def test_provider_registration(self): + from sqlit.domains.connections.providers.catalog import ( + get_provider_schema, + get_supported_db_types, + ) + + assert "databricks" in get_supported_db_types() + schema = get_provider_schema("databricks") + field_names = [f.name for f in schema.fields] + assert "server" in field_names + assert "http_path" in field_names + assert "auth_type" in field_names + assert "access_token" in field_names From 5279ecb32d4df914b0bcb7f545b0426b3c401f17 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Mon, 25 May 2026 17:43:21 +0200 Subject: [PATCH 2/6] fix(databricks): register databricks in DatabaseType enum --- sqlit/domains/connections/domain/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlit/domains/connections/domain/config.py b/sqlit/domains/connections/domain/config.py index 8ad839de..6c37675f 100644 --- a/sqlit/domains/connections/domain/config.py +++ b/sqlit/domains/connections/domain/config.py @@ -14,6 +14,7 @@ class DatabaseType(str, Enum): CLICKHOUSE = "clickhouse" COCKROACHDB = "cockroachdb" D1 = "d1" + DATABRICKS = "databricks" DUCKDB = "duckdb" DB2 = "db2" FIREBIRD = "firebird" @@ -53,6 +54,7 @@ class DatabaseType(str, Enum): DatabaseType.HANA, DatabaseType.TERADATA, DatabaseType.SNOWFLAKE, + DatabaseType.DATABRICKS, DatabaseType.BIGQUERY, DatabaseType.SPANNER, DatabaseType.TRINO, From f506cb91f6e40a8cb9a5a5eada7f9eb6bd3f03a1 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:32:19 +0200 Subject: [PATCH 3/6] fix(databricks): list all non-view Unity Catalog table types The information_schema filter used `table_type IN ('MANAGED', 'EXTERNAL', 'BASE TABLE')`. 'BASE TABLE' is not a Unity Catalog table_type at all, and the include-list hid FOREIGN (Lakehouse Federation), STREAMING_TABLE, MANAGED_SHALLOW_CLONE and EXTERNAL_SHALLOW_CLONE from the explorer tree. Invert it: tables are everything that is not VIEW or MATERIALIZED_VIEW, so table types Databricks adds later show up without another code change. get_views now reads the same information_schema.tables column instead of information_schema.views, which keeps the two lists complementary and picks up materialized views regardless of whether they appear in the views table. Claude-Session: https://claude.ai/code/session_01S6TsbrUgqAv3UETAfkg1ip --- .../providers/databricks/adapter.py | 13 +++++- tests/unit/test_databricks_adapter.py | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/sqlit/domains/connections/providers/databricks/adapter.py b/sqlit/domains/connections/providers/databricks/adapter.py index 2332a797..121fa5d5 100644 --- a/sqlit/domains/connections/providers/databricks/adapter.py +++ b/sqlit/domains/connections/providers/databricks/adapter.py @@ -24,6 +24,14 @@ from sqlit.domains.connections.domain.config import ConnectionConfig +# Unity Catalog `information_schema.tables.table_type` values that are views +# rather than tables. Everything else -- MANAGED, EXTERNAL, FOREIGN, +# STREAMING_TABLE, MANAGED_SHALLOW_CLONE, EXTERNAL_SHALLOW_CLONE and any type +# added later -- is listed as a table. +_VIEW_TABLE_TYPES = ("VIEW", "MATERIALIZED_VIEW") +_VIEW_TABLE_TYPES_SQL = ", ".join(f"'{t}'" for t in _VIEW_TABLE_TYPES) + + class DatabricksAdapter(CursorBasedAdapter): """Adapter for Databricks SQL warehouses and clusters.""" @@ -156,7 +164,7 @@ def get_tables(self, conn: Any, database: str | None = None) -> list[TableInfo]: cursor.execute( "SELECT table_schema, table_name FROM " f"{self.quote_identifier(database)}.information_schema.tables " - "WHERE table_type IN ('MANAGED', 'EXTERNAL', 'BASE TABLE') " + f"WHERE table_type NOT IN ({_VIEW_TABLE_TYPES_SQL}) " "ORDER BY table_schema, table_name" ) return [(row[0], row[1]) for row in cursor.fetchall()] @@ -169,7 +177,8 @@ def get_views(self, conn: Any, database: str | None = None) -> list[TableInfo]: if database: cursor.execute( "SELECT table_schema, table_name FROM " - f"{self.quote_identifier(database)}.information_schema.views " + f"{self.quote_identifier(database)}.information_schema.tables " + f"WHERE table_type IN ({_VIEW_TABLE_TYPES_SQL}) " "ORDER BY table_schema, table_name" ) return [(row[0], row[1]) for row in cursor.fetchall()] diff --git a/tests/unit/test_databricks_adapter.py b/tests/unit/test_databricks_adapter.py index 28420a26..9495ab21 100644 --- a/tests/unit/test_databricks_adapter.py +++ b/tests/unit/test_databricks_adapter.py @@ -159,6 +159,49 @@ def test_get_tables_with_catalog_uses_info_schema(self): assert "`main`.information_schema.tables" in sql assert tables == [("default", "trips"), ("analytics", "events")] + def test_get_tables_excludes_only_view_types(self): + """Tables list keeps FOREIGN/STREAMING_TABLE/shallow clones, drops views.""" + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [] + + adapter.get_tables(mock_conn, database="main") + sql = mock_cursor.execute.call_args[0][0] + + assert "table_type NOT IN ('VIEW', 'MATERIALIZED_VIEW')" in sql + # These are real Unity Catalog table_type values and must not be filtered out. + for table_type in ( + "MANAGED", + "EXTERNAL", + "FOREIGN", + "STREAMING_TABLE", + "MANAGED_SHALLOW_CLONE", + "EXTERNAL_SHALLOW_CLONE", + ): + assert table_type not in sql + # 'BASE TABLE' is not a Unity Catalog table_type. + assert "BASE TABLE" not in sql + + def test_get_views_reads_view_types_from_tables(self): + from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter + + adapter = DatabricksAdapter() + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [("default", "trips_daily")] + + views = adapter.get_views(mock_conn, database="main") + sql = mock_cursor.execute.call_args[0][0] + + assert "`main`.information_schema.tables" in sql + assert "table_type IN ('VIEW', 'MATERIALIZED_VIEW')" in sql + assert views == [("default", "trips_daily")] + def test_get_columns_uses_info_schema(self): from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter From 9ee22b2b178a811bdd12073a27a450dc9c9e5346 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:05:58 +0200 Subject: [PATCH 4/6] fix(connections): protect provider token credentials and migration --- .../domains/connections/app/persist_utils.py | 3 + sqlit/domains/connections/domain/config.py | 29 ++++- .../connections/domain/credential_aliases.py | 67 ++++++++++ .../domains/connections/store/connections.py | 23 +++- .../unit/test_provider_credential_aliases.py | 119 ++++++++++++++++++ 5 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 sqlit/domains/connections/domain/credential_aliases.py create mode 100644 tests/unit/test_provider_credential_aliases.py diff --git a/sqlit/domains/connections/app/persist_utils.py b/sqlit/domains/connections/app/persist_utils.py index c6c3babe..a9e9be17 100644 --- a/sqlit/domains/connections/app/persist_utils.py +++ b/sqlit/domains/connections/app/persist_utils.py @@ -21,6 +21,9 @@ def build_persist_connections( """ persist_connections = copy.deepcopy(connections) for conn in persist_connections: + from sqlit.domains.connections.domain.credential_aliases import normalize_credential_options + + normalize_credential_options(conn) endpoint = conn.tcp_endpoint if endpoint and endpoint.password is None and not endpoint.password_command: stored = credentials_service.get_password(conn.name) diff --git a/sqlit/domains/connections/domain/config.py b/sqlit/domains/connections/domain/config.py index 3767308a..012a8a2b 100644 --- a/sqlit/domains/connections/domain/config.py +++ b/sqlit/domains/connections/domain/config.py @@ -146,6 +146,11 @@ class ConnectionConfig: extra_options: dict[str, str] = field(default_factory=dict) options: dict[str, Any] = field(default_factory=dict) + def __post_init__(self) -> None: + from sqlit.domains.connections.domain.credential_aliases import normalize_credential_options + + normalize_credential_options(self) + @classmethod def from_dict(cls, data: Mapping[str, Any]) -> ConnectionConfig: payload = dict(data) @@ -264,10 +269,21 @@ def from_dict(cls, data: Mapping[str, Any]) -> ConnectionConfig: ) def get_option(self, name: str, default: Any | None = None) -> Any: + from sqlit.domains.connections.domain.credential_aliases import credential_option + + if name == credential_option(self) and self.tcp_endpoint is not None: + fallback = self.tcp_endpoint.password + return self.options.get(name, fallback if fallback is not None else default) return self.options.get(name, default) def set_option(self, name: str, value: Any) -> None: + from sqlit.domains.connections.domain.credential_aliases import credential_option, normalize_credential_options + + previous_credential = credential_option(self) self.options[name] = value + if previous_credential != credential_option(self) and self.tcp_endpoint is not None: + self.tcp_endpoint.password = None + normalize_credential_options(self) def get_field_value(self, name: str, default: Any = "") -> Any: values = self.to_form_values() @@ -319,17 +335,24 @@ def to_form_values(self) -> dict[str, Any]: values["ssh_enabled"] = "disabled" values.update(self.options) + from sqlit.domains.connections.domain.credential_aliases import credential_option + + alias = credential_option(self) + if alias: + values[alias] = self.get_option(alias) return values def to_dict(self, *, include_passwords: bool = True) -> dict[str, Any]: + from sqlit.domains.connections.domain.credential_aliases import public_connection_url, without_secret_options + data: dict[str, Any] = { "name": self.name, "db_type": self.db_type, "source": self.source, - "connection_url": self.connection_url, + "connection_url": self.connection_url if include_passwords else public_connection_url(self), "folder_path": self.folder_path, - "extra_options": dict(self.extra_options), - "options": dict(self.options), + "extra_options": dict(self.extra_options) if include_passwords else without_secret_options(self, self.extra_options), + "options": dict(self.options) if include_passwords else without_secret_options(self, self.options), } if isinstance(self.endpoint, FileEndpoint): diff --git a/sqlit/domains/connections/domain/credential_aliases.py b/sqlit/domains/connections/domain/credential_aliases.py new file mode 100644 index 00000000..b6342405 --- /dev/null +++ b/sqlit/domains/connections/domain/credential_aliases.py @@ -0,0 +1,67 @@ +"""Map mutually exclusive provider secrets onto the existing keyring credential. + +A Databricks/Exasol connection uses one authentication secret at a time. Keeping +it in endpoint.password reuses the credential store's save/load/rename semantics; +provider-specific field names remain available to forms and CLI configuration. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit + +if TYPE_CHECKING: + from typing import Any + + from sqlit.domains.connections.domain.config import ConnectionConfig + +_CREDENTIAL_MODES = { + 'databricks': ('auth_type', 'pat', {'pat': 'access_token', 'oauth-m2m': 'client_secret'}), + 'exasol': ('authenticator', 'password', {'access_token': 'access_token', 'refresh_token': 'refresh_token'}), +} + + +def credential_option(config: ConnectionConfig) -> str | None: + definition = _CREDENTIAL_MODES.get(config.db_type) + if definition is None: + return None + selector, default, aliases = definition + mode = config.options.get(selector, config.extra_options.get(selector, default)) + return aliases.get(mode) + + +def secret_option_names(config: ConnectionConfig) -> frozenset[str]: + definition = _CREDENTIAL_MODES.get(config.db_type) + return frozenset(definition[2].values()) if definition else frozenset() + + +def normalize_credential_options(config: ConnectionConfig) -> None: + definition = _CREDENTIAL_MODES.get(config.db_type) + if definition is None: + return + selector, _, _ = definition + config.options = dict(config.options) + config.extra_options = dict(config.extra_options) + if selector in config.extra_options: + config.options.setdefault(selector, config.extra_options.pop(selector)) + selected = credential_option(config) + for name in secret_option_names(config): + extra_value = config.extra_options.pop(name, None) + value = config.options.pop(name, extra_value) + if name == selected and value is not None and config.tcp_endpoint is not None: + config.tcp_endpoint.password = value + + +def without_secret_options(config: ConnectionConfig, options: dict[str, Any]) -> dict[str, Any]: + names = secret_option_names(config) + return {key: value for key, value in options.items() if key not in names} + + +def public_connection_url(config: ConnectionConfig) -> str | None: + url = config.connection_url + if not url or config.db_type not in _CREDENTIAL_MODES: + return url + parsed = urlsplit(url) + secret_names = secret_option_names(config) | {'password'} + query = [(key, value) for key, value in parse_qsl(parsed.query, keep_blank_values=True) + if key not in secret_names] + return urlunsplit(parsed._replace(netloc=parsed.netloc.rsplit('@', 1)[-1], query=urlencode(query))) diff --git a/sqlit/domains/connections/store/connections.py b/sqlit/domains/connections/store/connections.py index 20189d39..110b7568 100644 --- a/sqlit/domains/connections/store/connections.py +++ b/sqlit/domains/connections/store/connections.py @@ -79,19 +79,37 @@ def load_all(self, load_credentials: bool = True) -> list[ConnectionConfig]: return [] version, raw_connections, needs_migration = self._unpack_connections_payload(data) try: + from sqlit.domains.connections.domain.credential_aliases import credential_option, secret_option_names from sqlit.domains.connections.providers.config_service import normalize_connection_config configs = [] + migrated_provider_secret = False for conn in raw_connections: if not isinstance(conn, dict): continue config = ConnectionConfig.from_dict(conn) config = normalize_connection_config(config) + legacy_secrets = any( + source.get(key) + for source in (conn, conn.get("options") or {}, conn.get("extra_options") or {}) + if isinstance(source, dict) + for key in secret_option_names(config) + ) + if legacy_secrets: + # Move old plaintext token fields before rewriting any index. + # Otherwise saving an unrelated connection could redact the + # old token without ever storing it in the credential backend. + endpoint = config.tcp_endpoint + if credential_option(config) and endpoint and endpoint.password is not None: + self.credentials_service.set_password(config.name, endpoint.password) + migrated_provider_secret = True if load_credentials: # Retrieve passwords from credentials service self._load_credentials(config) configs.append(config) - if needs_migration: + if migrated_provider_secret: + self._write_index(configs) + elif needs_migration: self._migrate_connections_payload(raw_connections, version) return configs except (TypeError, KeyError): @@ -153,6 +171,9 @@ def _save_credentials(self, config: ConnectionConfig) -> list[CredentialsStoreEr Note: Empty string "" is a valid password (e.g., CockroachDB insecure mode). Only None means "delete/no password stored". """ + from sqlit.domains.connections.domain.credential_aliases import normalize_credential_options + + normalize_credential_options(config) errors: list[CredentialsStoreError] = [] endpoint = config.tcp_endpoint diff --git a/tests/unit/test_provider_credential_aliases.py b/tests/unit/test_provider_credential_aliases.py new file mode 100644 index 00000000..028c2690 --- /dev/null +++ b/tests/unit/test_provider_credential_aliases.py @@ -0,0 +1,119 @@ +"""Provider credentials must use the same protected slot as database passwords.""" +from __future__ import annotations + +import json +from dataclasses import replace + +import pytest + +from sqlit.domains.connections.app.credentials import PlaintextCredentialsService +from sqlit.domains.connections.domain.config import ConnectionConfig, TcpEndpoint +from sqlit.domains.connections.providers.registry import get_supported_db_types +from sqlit.domains.connections.store.connections import ConnectionStore + +CASES = [ + ('databricks', 'auth_type', 'pat', 'access_token'), + ('databricks', 'auth_type', 'oauth-m2m', 'client_secret'), + ('exasol', 'authenticator', 'access_token', 'access_token'), + ('exasol', 'authenticator', 'refresh_token', 'refresh_token'), +] + + +def config_for(provider, selector, mode, field): + return ConnectionConfig( + name='protected', db_type=provider, endpoint=TcpEndpoint(host='example.invalid'), + options={selector: mode, field: 'SYNTHETIC_SECRET', + 'http_path': '/sql/1.0/warehouses/example', 'client_id': 'example-client'}, + connection_url=f'{provider}://user:SYNTHETIC_URL_SECRET@example.invalid?{field}=SYNTHETIC_SECRET', + ) + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +def test_secret_uses_protected_slot_and_redacts_serialization(provider, selector, mode, field): + config = config_for(provider, selector, mode, field) + assert config.tcp_endpoint.password == 'SYNTHETIC_SECRET' + assert field not in config.options + assert config.get_option(field) == 'SYNTHETIC_SECRET' + assert config.to_form_values()[field] == 'SYNTHETIC_SECRET' + public = json.dumps(config.to_dict(include_passwords=False)) + assert 'SYNTHETIC_SECRET' not in public + assert 'SYNTHETIC_URL_SECRET' not in public + transported = ConnectionConfig.from_dict(config.to_dict()) + assert transported.get_option(field) == 'SYNTHETIC_SECRET' + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +def test_store_reload_and_rename_keep_secret_out_of_index(tmp_path, provider, selector, mode, field): + if provider not in get_supported_db_types(): + pytest.skip('Provider is not part of this branch') + credentials = PlaintextCredentialsService() + path = tmp_path / 'connections.json' + store = ConnectionStore(credentials, file_path=path) + config = config_for(provider, selector, mode, field) + store.save_one(config) + assert 'SYNTHETIC' not in path.read_text() + assert credentials.get_password('protected') == 'SYNTHETIC_SECRET' + loaded = store.load_all()[0] + assert loaded.get_option(field) == 'SYNTHETIC_SECRET' + renamed = replace(store.load_all(load_credentials=False)[0], name='renamed') + store.save_one(renamed, previous_name='protected') + assert credentials.get_password('protected') is None + assert store.load_all()[0].get_option(field) == 'SYNTHETIC_SECRET' + assert 'SYNTHETIC' not in path.read_text() + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +def test_legacy_extra_options_secrets_are_protected(provider, selector, mode, field): + config = ConnectionConfig(name='legacy', db_type=provider, + options={selector: mode}, extra_options={field: 'SYNTHETIC_SECRET'}) + assert config.tcp_endpoint.password == 'SYNTHETIC_SECRET' + assert field not in config.extra_options + assert 'SYNTHETIC_SECRET' not in json.dumps(config.to_dict(include_passwords=False)) + + +def test_switching_authentication_does_not_reuse_a_different_secret(): + config = config_for('databricks', 'auth_type', 'pat', 'access_token') + config.set_option('auth_type', 'oauth-m2m') + assert config.get_option('client_secret') is None + config.set_option('client_secret', 'NEW_SECRET') + assert config.tcp_endpoint.password == 'NEW_SECRET' + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +def test_saving_another_connection_preserves_legacy_token(tmp_path, provider, selector, mode, field): + if provider not in get_supported_db_types(): + pytest.skip('Provider is not part of this branch') + config = config_for(provider, selector, mode, field) + legacy = config.to_dict() + legacy['endpoint']['password'] = None + legacy['options'][field] = 'LEGACY_SECRET' + path = tmp_path / 'connections.json' + path.write_text(json.dumps({'version': 2, 'connections': [legacy]})) + credentials = PlaintextCredentialsService() + store = ConnectionStore(credentials, file_path=path) + other = ConnectionConfig(name='unrelated', db_type='postgresql', endpoint=TcpEndpoint(host='localhost')) + store.save_one(other) + assert credentials.get_password('protected') == 'LEGACY_SECRET' + assert 'LEGACY_SECRET' not in path.read_text() + assert store.get_by_name('protected').get_option(field) == 'LEGACY_SECRET' + + +def test_failed_legacy_migration_preserves_original_file(tmp_path): + from sqlit.domains.connections.app.credentials import CredentialsStoreError + + provider, selector, mode, field = next(case for case in CASES if case[0] in get_supported_db_types()) + legacy = config_for(provider, selector, mode, field).to_dict() + legacy['endpoint']['password'] = None + legacy['options'][field] = 'LEGACY_SECRET' + path = tmp_path / 'connections.json' + original = json.dumps({'version': 2, 'connections': [legacy]}) + path.write_text(original) + + class UnavailableCredentials(PlaintextCredentialsService): + def set_password(self, name, password): + raise CredentialsStoreError(connection_name=name, kind='db', action='store', reason=RuntimeError('locked')) + + store = ConnectionStore(UnavailableCredentials(), file_path=path) + with pytest.raises(CredentialsStoreError): + store.load_all() + assert path.read_text() == original From 9ac19c5f4fcead42fbe4b35f40bb15aef945d959 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:48:59 +0200 Subject: [PATCH 5/6] fix(connections): cover token editing and cross-process persistence --- .../connections/domain/credential_aliases.py | 8 +- .../domains/connections/store/connections.py | 12 +- sqlit/domains/connections/ui/restart_cache.py | 12 ++ .../test_cloud_provider_credentials.py | 115 ++++++++++++++++++ tests/ui/test_provider_token_credentials.py | 32 +++++ .../unit/test_provider_credential_aliases.py | 36 ++++++ 6 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 tests/integration/test_cloud_provider_credentials.py create mode 100644 tests/ui/test_provider_token_credentials.py diff --git a/sqlit/domains/connections/domain/credential_aliases.py b/sqlit/domains/connections/domain/credential_aliases.py index b6342405..59f336ae 100644 --- a/sqlit/domains/connections/domain/credential_aliases.py +++ b/sqlit/domains/connections/domain/credential_aliases.py @@ -48,7 +48,13 @@ def normalize_credential_options(config: ConnectionConfig) -> None: extra_value = config.extra_options.pop(name, None) value = config.options.pop(name, extra_value) if name == selected and value is not None and config.tcp_endpoint is not None: - config.tcp_endpoint.password = value + config.tcp_endpoint.password = value or None + + +def credential_kind_changed(previous: ConnectionConfig | None, current: ConnectionConfig) -> bool: + if previous is None or not (secret_option_names(previous) or secret_option_names(current)): + return False + return (previous.db_type, credential_option(previous)) != (current.db_type, credential_option(current)) def without_secret_options(config: ConnectionConfig, options: dict[str, Any]) -> dict[str, Any]: diff --git a/sqlit/domains/connections/store/connections.py b/sqlit/domains/connections/store/connections.py index 110b7568..474c5864 100644 --- a/sqlit/domains/connections/store/connections.py +++ b/sqlit/domains/connections/store/connections.py @@ -273,10 +273,14 @@ def save_all(self, connections: list[ConnectionConfig]) -> None: connections: List of ConnectionConfig objects to save. """ from sqlit.domains.connections.app.persist_utils import build_persist_connections + from sqlit.domains.connections.domain.credential_aliases import credential_kind_changed errors: list[CredentialsStoreError] = [] + existing = {config.name: config for config in self.load_all(load_credentials=False)} persist_connections = build_persist_connections(connections, self.credentials_service) - for config in persist_connections: + for source, config in zip(connections, persist_connections, strict=True): + if credential_kind_changed(existing.get(source.name), source) and source.tcp_endpoint and source.tcp_endpoint.password is None and config.tcp_endpoint is not None: + config.tcp_endpoint.password = None errors.extend(self._save_credentials(config)) self._write_index(persist_connections) @@ -302,10 +306,13 @@ def save_one( previous_name: The connection's prior name when renaming. """ from sqlit.domains.connections.app.persist_utils import build_persist_connections + from sqlit.domains.connections.domain.credential_aliases import credential_kind_changed renamed = bool(previous_name and previous_name != connection.name) existing = self.load_all(load_credentials=False) + previous = next((config for config in existing if config.name == (previous_name or connection.name)), None) + credential_changed = credential_kind_changed(previous, connection) filtered = [ c for c in existing @@ -324,6 +331,7 @@ def save_one( endpoint and endpoint.password is None and not endpoint.password_command + and not credential_changed ): endpoint.password = self.credentials_service.get_password_for_migration(previous_name) # type: ignore[arg-type] if ( @@ -383,7 +391,7 @@ def save_one( errors.append(exc) else: self._write_index(filtered) - target = build_persist_connections([connection], self.credentials_service)[0] + target = copy.deepcopy(connection) if credential_changed else build_persist_connections([connection], self.credentials_service)[0] errors.extend(self._save_credentials(target)) if errors: raise CredentialsPersistError(errors) diff --git a/sqlit/domains/connections/ui/restart_cache.py b/sqlit/domains/connections/ui/restart_cache.py index e2daa69b..b316a39e 100644 --- a/sqlit/domains/connections/ui/restart_cache.py +++ b/sqlit/domains/connections/ui/restart_cache.py @@ -16,6 +16,18 @@ def get_restart_cache_path() -> Path: def write_restart_cache(payload: dict[str, Any]) -> None: """Persist restart cache payload to disk (best effort).""" try: + values = payload.get("values") + if isinstance(values, dict) and values.get("db_type") in {"databricks", "exasol"}: + from sqlit.domains.connections.domain.config import ConnectionConfig + from sqlit.domains.connections.domain.credential_aliases import secret_option_names + + config = ConnectionConfig.from_dict(values) + public = config.to_dict(include_passwords=False) + hidden = secret_option_names(config) | {"password", "ssh_password"} + safe_values = {key: value for key, value in values.items() if key not in hidden} + safe_values["extra_options"] = public["extra_options"] + safe_values["connection_url"] = public["connection_url"] + payload = {**payload, "values": safe_values} get_restart_cache_path().write_text(json.dumps(payload), encoding="utf-8") except Exception: # Best-effort; don't block installation due to caching failure. diff --git a/tests/integration/test_cloud_provider_credentials.py b/tests/integration/test_cloud_provider_credentials.py new file mode 100644 index 00000000..4e432c39 --- /dev/null +++ b/tests/integration/test_cloud_provider_credentials.py @@ -0,0 +1,115 @@ +"""Opt-in real-service proof of CLI, protected persistence and adapter behavior. + +Set SQLIT_LIVE_PROVIDER (databricks/exasol), SQLIT_LIVE_HOST and +SQLIT_LIVE_TOKEN. Databricks also needs SQLIT_LIVE_HTTP_PATH; Exasol needs +SQLIT_LIVE_USERNAME. The token is read only from the environment and passed to +CLI creation on stdin. The CLI must use a working OS keyring, never plaintext. +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +import uuid +from dataclasses import replace +from pathlib import Path +from urllib.parse import quote, urlencode + +import pytest + +from sqlit.domains.connections.app.credentials import KeyringCredentialsService, is_keyring_usable +from sqlit.domains.connections.providers.registry import get_adapter, get_supported_db_types +from sqlit.domains.connections.store.connections import ConnectionStore + + +def test_saved_cloud_connection_survives_cli_reload_and_rename(tmp_path): + provider = os.environ.get('SQLIT_LIVE_PROVIDER') + if not provider: + pytest.skip('Set SQLIT_LIVE_PROVIDER to run against an owned cloud test database') + if provider not in {'databricks', 'exasol'} or provider not in get_supported_db_types(): + pytest.fail('Requested cloud provider is not available in this branch') + required = ['SQLIT_LIVE_HOST', 'SQLIT_LIVE_TOKEN'] + required.append('SQLIT_LIVE_HTTP_PATH' if provider == 'databricks' else 'SQLIT_LIVE_USERNAME') + missing = [name for name in required if not os.environ.get(name)] + if missing: + pytest.fail(f'Missing live-test configuration: {missing}') + if not is_keyring_usable(): + pytest.fail('Live saved-connection proof requires a working OS keyring') + + secret = os.environ['SQLIT_LIVE_TOKEN'] + host = os.environ['SQLIT_LIVE_HOST'] + catalog = os.environ.get('SQLIT_LIVE_CATALOG', 'workspace') if provider == 'databricks' else '' + user = 'token' if provider == 'databricks' else os.environ['SQLIT_LIVE_USERNAME'] + port = '' if provider == 'databricks' else ':' + os.environ.get('SQLIT_LIVE_PORT', '8563') + query = urlencode({'http_path': os.environ['SQLIT_LIVE_HTTP_PATH']}) if provider == 'databricks' else '' + url = f'{provider}://{quote(user, safe="")}:{quote(secret, safe="")}@{host}{port}/{catalog}' + if query: + url += '?' + query + name = 'sqlit-live-' + uuid.uuid4().hex + renamed = name + '-renamed' + env = dict(os.environ, SQLIT_CONFIG_DIR=str(tmp_path)) + # Do not forward the credential environment variable to CLI processes. + env.pop('SQLIT_LIVE_TOKEN', None) + (tmp_path / 'settings.json').write_text('{"allow_plaintext_credentials": false}') + credentials = KeyringCredentialsService() + store = ConnectionStore(credentials, file_path=tmp_path / 'connections.json') + repo = Path(__file__).resolve().parents[2] + + def cli(*args, input_text=None): + process = subprocess.run([sys.executable, '-m', 'sqlit.cli', *args], + cwd=repo, env=env, input=input_text, text=True, capture_output=True, timeout=180) + output = (process.stdout + process.stderr).replace(secret, '[REDACTED]') + assert process.returncode == 0, output + return process.stdout + + def assert_index_is_protected(): + leaked = secret in (tmp_path / 'connections.json').read_text() + assert not leaked, 'Credential leaked into the saved connection index' + assert not (tmp_path / 'credentials.json').exists(), 'Unexpected plaintext credential file' + + conn = None + created = False + adapter = get_adapter(provider) + schema = ('SQLIT_TEST_' + uuid.uuid4().hex[:12]).lower() if provider == 'databricks' else 'SQLIT_TEST_' + uuid.uuid4().hex[:12].upper() + q = adapter.quote_identifier + namespace = f'{q(catalog)}.{q(schema)}' if catalog else q(schema) + table_name = 'probe' if provider == 'databricks' else 'PROBE' + view_name = 'probe_view' if provider == 'databricks' else 'PROBE_VIEW' + table = namespace + '.' + q(table_name) + try: + cli('connections', 'add', '--name', name, '--url-stdin', input_text=url + '\n') + assert_index_is_protected() + result = json.loads(cli('query', '-c', name, '-q', 'SELECT 1 AS probe', '--format', 'json')) + assert list(result[0].values()) == [1] + config = store.get_by_name(name) + assert config is not None + conn = adapter.connect(config) + adapter.execute_non_query(conn, f'CREATE SCHEMA {namespace}') + created = True + adapter.execute_non_query(conn, f'CREATE TABLE {table} (id INTEGER, label VARCHAR(40))') + adapter.execute_non_query(conn, f"INSERT INTO {table} VALUES (1, 'alpha'), (2, 'beta'), (3, NULL)") + _, rows, truncated = adapter.execute_query(conn, f'SELECT id, label FROM {table} ORDER BY id', max_rows=2) + assert rows == [(1, 'alpha'), (2, 'beta')] + assert truncated + adapter.execute_non_query(conn, f'CREATE VIEW {namespace}.{q(view_name)} AS SELECT * FROM {table}') + assert (schema, table_name) in adapter.get_tables(conn, catalog or None) + assert (schema, view_name) in adapter.get_views(conn, catalog or None) + columns = adapter.get_columns(conn, table_name, catalog or None, schema) + assert [column.name.lower() for column in columns] == ['id', 'label'] + store.save_one(replace(store.load_all(load_credentials=False)[0], name=renamed), previous_name=name) + assert_index_is_protected() + result = json.loads(cli('query', '-c', renamed, '-q', f'SELECT COUNT(*) AS n FROM {table}', '--format', 'json')) + assert list(result[0].values()) == [3] + finally: + try: + if conn is not None: + try: + if created: + adapter.execute_non_query(conn, f'DROP SCHEMA {namespace} CASCADE') + finally: + conn.close() + finally: + for connection_name in (name, renamed): + store.delete(connection_name) + credentials.delete_all_for_connection(connection_name) diff --git a/tests/ui/test_provider_token_credentials.py b/tests/ui/test_provider_token_credentials.py new file mode 100644 index 00000000..5ce95ed4 --- /dev/null +++ b/tests/ui/test_provider_token_credentials.py @@ -0,0 +1,32 @@ +"""Connection forms must preserve protected token credentials without extra prompts.""" +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from sqlit.domains.connections.providers.registry import get_supported_db_types +from tests.ui.conftest import ConnectionScreenTestApp +from tests.unit.test_provider_credential_aliases import CASES, config_for + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +async def test_token_form_round_trip_and_test_connection(provider, selector, mode, field, monkeypatch): + if provider not in get_supported_db_types(): + pytest.skip('Provider is not part of this branch') + cfg = config_for(provider, selector, mode, field) + app = ConnectionScreenTestApp(config=cfg, editing=True) + async with app.run_test(size=(120, 45)) as pilot: + screen = app.screen + await pilot.pause() + assert screen.query_one(f'#field-{field}').value == 'SYNTHETIC_SECRET' + screen.query_one(f'#field-{field}').value = 'EDITED_SECRET' + await pilot.pause() + tested = [] + monkeypatch.setattr(screen, '_driver_status_controller', lambda: SimpleNamespace(missing_driver_error=None)) + monkeypatch.setattr(screen, '_run_test', tested.append) + screen.action_test_connection() + assert len(tested) == 1, 'Token auth opened an unrelated password prompt' + assert tested[0].get_option(field) == 'EDITED_SECRET' + assert field not in tested[0].options + assert tested[0].tcp_endpoint.password == 'EDITED_SECRET' diff --git a/tests/unit/test_provider_credential_aliases.py b/tests/unit/test_provider_credential_aliases.py index 028c2690..3199fc70 100644 --- a/tests/unit/test_provider_credential_aliases.py +++ b/tests/unit/test_provider_credential_aliases.py @@ -117,3 +117,39 @@ def set_password(self, name, password): with pytest.raises(CredentialsStoreError): store.load_all() assert path.read_text() == original + + +@pytest.mark.parametrize('save_all', [False, True]) +def test_auth_mode_change_without_a_new_secret_clears_old_credential(tmp_path, save_all): + provider = 'databricks' if 'databricks' in get_supported_db_types() else 'exasol' + selector = 'auth_type' if provider == 'databricks' else 'authenticator' + old_mode, new_mode, new_field = ('pat', 'oauth-m2m', 'client_secret') if provider == 'databricks' else ('access_token', 'refresh_token', 'refresh_token') + credentials = PlaintextCredentialsService() + store = ConnectionStore(credentials, file_path=tmp_path / 'connections.json') + store.save_one(config_for(provider, selector, old_mode, 'access_token')) + changed = store.load_all(load_credentials=False)[0] + changed.set_option(selector, new_mode) + if save_all: + store.save_all([changed]) + else: + store.save_one(changed) + assert credentials.get_password(changed.name) is None + assert store.load_all()[0].get_option(new_field) is None + + +def test_empty_token_field_means_no_new_credential(): + config = ConnectionConfig(name='blank', db_type='databricks', options={'access_token': ''}) + assert config.tcp_endpoint.password is None + + +@pytest.mark.parametrize(('provider', 'selector', 'mode', 'field'), CASES) +def test_driver_restart_cache_does_not_persist_provider_secrets(tmp_path, monkeypatch, provider, selector, mode, field): + from sqlit.domains.connections.ui import restart_cache + + path = tmp_path / 'restart.json' + monkeypatch.setattr(restart_cache, 'get_restart_cache_path', lambda: path) + values = config_for(provider, selector, mode, field).to_form_values() + restart_cache.write_restart_cache({'version': 1, 'values': values}) + assert 'SYNTHETIC_SECRET' not in path.read_text() + assert 'SYNTHETIC_URL_SECRET' not in path.read_text() + assert values[field] == 'SYNTHETIC_SECRET' From 93d3f78d53e3c8914725e20796204a2d223e8b9c Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:57:51 +0200 Subject: [PATCH 6/6] fix(databricks): complete authentication and catalog integration --- .github/workflows/ci.yml | 16 + CONTRIBUTING.md | 18 + README.md | 11 +- pyproject.toml | 3 +- sqlit/domains/connections/cli/prompts.py | 15 +- sqlit/domains/connections/domain/passwords.py | 3 + .../providers/databricks/adapter.py | 120 ++-- tests/unit/test_databricks_regressions.py | 107 ++++ uv.lock | 536 +++++++++++++++++- 9 files changed, 749 insertions(+), 80 deletions(-) create mode 100644 tests/unit/test_databricks_regressions.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7127e0db..a98e39c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,22 @@ jobs: --ignore=tests/test_ssh.py \ --ignore=tests/test_clickhouse.py + test-databricks: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@v5 + - name: Install Databricks including OAuth SDK + run: uv sync --group test --no-dev --extra databricks + - name: Verify SDK and adapter regressions + run: | + uv run --no-sync python -c "from databricks.sdk.core import Config, oauth_service_principal" + uv run --no-sync pytest tests/unit/test_databricks_adapter.py tests/unit/test_databricks_regressions.py tests/unit/test_provider_credential_aliases.py tests/ui/test_provider_token_credentials.py -v --timeout=60 + test-sqlite: runs-on: ubuntu-latest strategy: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a66f78ca..9bd27332 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -262,3 +262,21 @@ sqlit should provide fun and a feeling of mastery and satisfaction for those who **Example:** = explorer pane, = query pane, = results pane. Rationale: E;Q;R satisfies both intuitiveness (each binding is the first letter of the pane), harmony (proximity: qwerty speaks for itself) + +### Cloud credential regression test + +`tests/integration/test_cloud_provider_credentials.py` is an opt-in test against an owned cloud +database. It requires a working OS keyring and creates a temporary schema and connection, then +removes them. It verifies CLI creation via stdin, separate-process queries, credential redaction, +metadata, row limits and rename. Load the test token from your secret manager into the process +environment; do not commit it or pass it as a command-line argument. + +Set `SQLIT_LIVE_PROVIDER=databricks`, `SQLIT_LIVE_HOST`, `SQLIT_LIVE_HTTP_PATH`, +`SQLIT_LIVE_TOKEN`, and optionally `SQLIT_LIVE_CATALOG` (default `workspace`), then run: + +```bash +uv run --no-sync pytest tests/integration/test_cloud_provider_credentials.py -v --timeout=240 +``` + +The ordinary CI lane runs without cloud credentials. A configured live run fails on missing +configuration or an unavailable keyring; it does not silently skip those checks. diff --git a/README.md b/README.md index d9ab12e4..cf4be139 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,15 @@ Edit the `keymap.json` file in your sqlit config dir. See [`config/keymap.templa ## FAQ +### Databricks authentication + +The Databricks extra includes the SQL connector and the SDK required for service-principal OAuth. +PATs and client secrets are kept in the OS credential store, including connections created with +`connections add --url-stdin`. A PAT URL has this shape: +`databricks://token:TOKEN@HOST/CATALOG?http_path=/sql/1.0/warehouses/WAREHOUSE_ID`. +Use stdin to keep the URL out of shell history and process arguments. Browser OAuth does not ask +for a database password. Unity Catalog and the legacy Hive metastore use their respective metadata APIs. + ### How are sensitive credentials stored? Connection details are stored in `connections.json` inside the config directory, but passwords are stored in your OS keyring when available (macOS Keychain, Windows Credential Locker, Linux Secret Service). @@ -287,7 +296,7 @@ Most of the time you can just run `sqlit` and connect. If a Python driver is mis | Turso | `libsql` | `pipx inject sqlit-tui libsql` | `python -m pip install libsql` | | Cloudflare D1 | `requests` | `pipx inject sqlit-tui requests` | `python -m pip install requests` | | Snowflake | `snowflake-connector-python` | `pipx inject sqlit-tui snowflake-connector-python` | `python -m pip install snowflake-connector-python` | -| Databricks | `databricks-sql-connector` | `pipx inject sqlit-tui databricks-sql-connector` | `python -m pip install databricks-sql-connector` | +| Databricks | `databricks-sql-connector` | `pipx inject sqlit-tui databricks-sql-connector databricks-sdk` | `python -m pip install databricks-sql-connector databricks-sdk` | | Firebird | `firebirdsql` | `pipx inject sqlit-tui firebirdsql` | `python -m pip install firebirdsql` | | Athena | `pyathena` | `pipx inject sqlit-tui pyathena` | `python -m pip install pyathena` | | BigQuery | `google-cloud-bigquery` | `pipx inject sqlit-tui google-cloud-bigquery` | `python -m pip install google-cloud-bigquery` | diff --git a/pyproject.toml b/pyproject.toml index 7cf79931..2d94f042 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ all = [ "osquery>=3.0.0", "surrealdb>=1.0.0", "databricks-sql-connector>=3.0.0", + "databricks-sdk>=0.18.0", ] postgres = ["psycopg2-binary>=2.9.0"] cockroachdb = ["psycopg2-binary>=2.9.0"] @@ -91,7 +92,7 @@ flight = ["adbc-driver-flightsql>=1.0.0"] impala = ["impyla>=0.18.0"] osquery = ["osquery>=3.0.0"] surrealdb = ["surrealdb>=1.0.0"] -databricks = ["databricks-sql-connector>=3.0.0"] +databricks = ["databricks-sql-connector>=3.0.0", "databricks-sdk>=0.18.0"] ssh = [ "sshtunnel>=0.4.0", "paramiko>=2.0.0,<4.0.0", diff --git a/sqlit/domains/connections/cli/prompts.py b/sqlit/domains/connections/cli/prompts.py index 3d1b11ca..047832eb 100644 --- a/sqlit/domains/connections/cli/prompts.py +++ b/sqlit/domains/connections/cli/prompts.py @@ -24,21 +24,8 @@ def _needs_ssh_prompt(config: ConnectionConfig) -> bool: def _needs_db_prompt(config: ConnectionConfig) -> bool: """Check if DB password is still missing (ignoring password_command).""" - from sqlit.domains.connections.providers.metadata import is_file_based, requires_auth - - if is_file_based(config.db_type): - return False - if not requires_auth(config.db_type): - return False - auth_type = config.get_option("auth_type") - if auth_type in ("ad_default", "ad_integrated", "windows"): - return False - if config.db_type == "trino": - auth_method = str(config.options.get("trino_auth_method", config.extra_options.get("trino_auth_method", "basic"))).lower() - if auth_method in {"none", "kerberos", "gssapi"}: - return False endpoint = config.tcp_endpoint - return bool(endpoint and endpoint.password is None) + return bool(uses_db_password(config) and endpoint and endpoint.password is None) def prompt_for_password(config: ConnectionConfig) -> ConnectionConfig: diff --git a/sqlit/domains/connections/domain/passwords.py b/sqlit/domains/connections/domain/passwords.py index 3339f1f9..31fb5ab4 100644 --- a/sqlit/domains/connections/domain/passwords.py +++ b/sqlit/domains/connections/domain/passwords.py @@ -18,6 +18,9 @@ def uses_db_password(config: ConnectionConfig) -> bool: if auth_type in ("ad_default", "ad_integrated", "windows"): return False + if config.db_type == "databricks" and auth_type == "oauth-u2m": + return False + if config.db_type == "postgresql": from sqlit.domains.connections.providers.postgresql.auth import ( POSTGRES_AUTH_AZURE_ENTRA_CLI, diff --git a/sqlit/domains/connections/providers/databricks/adapter.py b/sqlit/domains/connections/providers/databricks/adapter.py index 121fa5d5..f8389bd8 100644 --- a/sqlit/domains/connections/providers/databricks/adapter.py +++ b/sqlit/domains/connections/providers/databricks/adapter.py @@ -9,6 +9,7 @@ from __future__ import annotations +from contextlib import closing from typing import TYPE_CHECKING, Any from sqlit.domains.connections.providers.adapters.base import ( @@ -92,6 +93,13 @@ def apply_database_override(self, config: ConnectionConfig, database: str) -> Co return config return config.with_endpoint(database=database) + def normalize_config(self, config: ConnectionConfig) -> ConnectionConfig: + # URL query parameters arrive as extra_options, before schema validation. + for name in ("http_path", "schema", "client_id", "auth_type"): + if name in config.extra_options: + config.options.setdefault(name, config.extra_options.pop(name)) + return config + def connect(self, config: ConnectionConfig) -> Any: sql_module = self._import_driver_module( "databricks.sql", @@ -131,11 +139,15 @@ def connect(self, config: ConnectionConfig) -> Any: connect_args["auth_type"] = "databricks-oauth" elif auth_type == "oauth-m2m": client_id = extras.get("client_id") - client_secret = extras.get("client_secret") + client_secret = config.get_option("client_secret") if not client_id or not client_secret: raise ValueError( "Databricks OAuth (Service Principal) requires client_id and client_secret." ) + self._import_driver_module( + "databricks.sdk.core", driver_name="Databricks OAuth M2M", + extra_name=self.install_extra, package_name="databricks-sdk", + ) connect_args["credentials_provider"] = _build_m2m_credentials_provider( endpoint.host, client_id, client_secret ) @@ -152,62 +164,76 @@ def connect(self, config: ConnectionConfig) -> Any: def get_databases(self, conn: Any) -> list[str]: """List Unity Catalog catalogs.""" - cursor = conn.cursor() - # SHOW CATALOGS is universally supported and avoids needing - # SELECT privilege on system.information_schema. - cursor.execute("SHOW CATALOGS") - return [row[0] for row in cursor.fetchall()] + with closing(conn.cursor()) as cursor: + # SHOW CATALOGS is universally supported and avoids needing + # SELECT privilege on system.information_schema. + cursor.execute("SHOW CATALOGS") + return [row[0] for row in cursor.fetchall()] def get_tables(self, conn: Any, database: str | None = None) -> list[TableInfo]: - cursor = conn.cursor() - if database: - cursor.execute( - "SELECT table_schema, table_name FROM " - f"{self.quote_identifier(database)}.information_schema.tables " - f"WHERE table_type NOT IN ({_VIEW_TABLE_TYPES_SQL}) " - "ORDER BY table_schema, table_name" - ) - return [(row[0], row[1]) for row in cursor.fetchall()] + with closing(conn.cursor()) as cursor: + if database == "hive_metastore": + cursor.tables(catalog_name=database, table_types=["TABLE"]) + return [(row.TABLE_SCHEM, row.TABLE_NAME) for row in cursor.fetchall() + if database == row.TABLE_CAT] + if database: + cursor.execute( + "SELECT table_schema, table_name FROM " + f"{self.quote_identifier(database)}.information_schema.tables " + f"WHERE table_type NOT IN ({_VIEW_TABLE_TYPES_SQL}) " + "ORDER BY table_schema, table_name" + ) + return [(row[0], row[1]) for row in cursor.fetchall()] - cursor.execute("SHOW TABLES") - return [(row[0], row[1]) for row in cursor.fetchall()] + cursor.execute("SHOW TABLES") + return [(row[0], row[1]) for row in cursor.fetchall()] def get_views(self, conn: Any, database: str | None = None) -> list[TableInfo]: - cursor = conn.cursor() - if database: - cursor.execute( - "SELECT table_schema, table_name FROM " - f"{self.quote_identifier(database)}.information_schema.tables " - f"WHERE table_type IN ({_VIEW_TABLE_TYPES_SQL}) " - "ORDER BY table_schema, table_name" - ) - return [(row[0], row[1]) for row in cursor.fetchall()] + with closing(conn.cursor()) as cursor: + if database == "hive_metastore": + cursor.tables(catalog_name=database, table_types=["VIEW"]) + return [(row.TABLE_SCHEM, row.TABLE_NAME) for row in cursor.fetchall() + if database == row.TABLE_CAT] + if database: + cursor.execute( + "SELECT table_schema, table_name FROM " + f"{self.quote_identifier(database)}.information_schema.tables " + f"WHERE table_type IN ({_VIEW_TABLE_TYPES_SQL}) " + "ORDER BY table_schema, table_name" + ) + return [(row[0], row[1]) for row in cursor.fetchall()] - cursor.execute("SHOW VIEWS") - # SHOW VIEWS columns: database, viewName, isTemporary - return [(row[0], row[1]) for row in cursor.fetchall()] + cursor.execute("SHOW VIEWS") + # SHOW VIEWS columns: database, viewName, isTemporary + return [(row[0], row[1]) for row in cursor.fetchall()] def get_columns( self, conn: Any, table: str, database: str | None = None, schema: str | None = None ) -> list[ColumnInfo]: - cursor = conn.cursor() - schema_name = schema or self.default_schema - if database: - cursor.execute( - "SELECT column_name, data_type FROM " - f"{self.quote_identifier(database)}.information_schema.columns " - "WHERE table_schema = ? AND table_name = ? " - "ORDER BY ordinal_position", - (schema_name, table), - ) - else: - cursor.execute( - "SELECT column_name, data_type FROM information_schema.columns " - "WHERE table_schema = ? AND table_name = ? " - "ORDER BY ordinal_position", - (schema_name, table), - ) - return [ColumnInfo(name=row[0], data_type=row[1]) for row in cursor.fetchall()] + with closing(conn.cursor()) as cursor: + schema_name = schema or self.default_schema + if database == "hive_metastore": + cursor.columns(catalog_name=database, schema_name=schema_name, table_name=table) + # Connector metadata parameters are patterns, not literal names. + return [ColumnInfo(name=row.COLUMN_NAME, data_type=row.TYPE_NAME) + for row in cursor.fetchall() + if database == row.TABLE_CAT and schema_name == row.TABLE_SCHEM and table == row.TABLE_NAME] + if database: + cursor.execute( + "SELECT column_name, data_type FROM " + f"{self.quote_identifier(database)}.information_schema.columns " + "WHERE table_schema = ? AND table_name = ? " + "ORDER BY ordinal_position", + (schema_name, table), + ) + else: + cursor.execute( + "SELECT column_name, data_type FROM information_schema.columns " + "WHERE table_schema = ? AND table_name = ? " + "ORDER BY ordinal_position", + (schema_name, table), + ) + return [ColumnInfo(name=row[0], data_type=row[1]) for row in cursor.fetchall()] def get_procedures(self, conn: Any, database: str | None = None) -> list[str]: return [] diff --git a/tests/unit/test_databricks_regressions.py b/tests/unit/test_databricks_regressions.py new file mode 100644 index 00000000..69ba7f9a --- /dev/null +++ b/tests/unit/test_databricks_regressions.py @@ -0,0 +1,107 @@ +"""Regressions in Databricks configuration, auth, and legacy catalog browsing.""" +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from sqlit.domains.connections.app.url_parser import parse_connection_url +from sqlit.domains.connections.cli.prompts import _needs_db_prompt, prompt_for_password +from sqlit.domains.connections.domain.passwords import needs_db_password +from sqlit.domains.connections.providers.databricks.adapter import DatabricksAdapter +from tests.helpers import ConnectionConfig + + +@pytest.mark.parametrize('auth', ['pat', 'oauth-u2m', 'oauth-m2m']) +def test_valid_auth_does_not_prompt_for_an_unrelated_database_password(auth): + cfg = ConnectionConfig(name='test', db_type='databricks', server='host', options={ + 'auth_type': auth, 'access_token': 'PAT', 'client_id': 'client', 'client_secret': 'SECRET', + 'http_path': '/sql/1.0/warehouses/example'}) + assert not needs_db_password(cfg) + assert not _needs_db_prompt(cfg) + with patch('getpass.getpass', side_effect=AssertionError('Unexpected password prompt')): + assert prompt_for_password(cfg) is cfg + + +def test_browser_auth_does_not_execute_password_command(): + cfg = ConnectionConfig(name='test', db_type='databricks', server='host', + options={'auth_type': 'oauth-u2m'}, password_command='should-not-run') + with patch('sqlit.domains.connections.cli.prompts.run_password_command', side_effect=AssertionError('Unexpected password command')): + prompt_for_password(cfg) + + +@pytest.mark.parametrize('auth_query', ['', '&auth_type=oauth-m2m&client_id=client&client_secret=SECRET']) +def test_url_parameters_reach_validation_and_secrets_are_redacted(auth_query): + cfg = parse_connection_url('databricks://token:PAT@host/workspace?http_path=%2Fsql%2F1.0%2Fwarehouses%2Fexample&schema=demo' + auth_query) + assert cfg.get_option('http_path') == '/sql/1.0/warehouses/example' + assert cfg.get_option('schema') == 'demo' + assert cfg.tcp_endpoint.database == 'workspace' + assert 'PAT' not in str(cfg.to_dict(include_passwords=False)) + assert 'SECRET' not in str(cfg.to_dict(include_passwords=False)) + + +def test_m2m_uses_secret_loaded_from_protected_credential_slot(): + cfg = ConnectionConfig(name='test', db_type='databricks', server='host', password='SECRET', + options={'auth_type': 'oauth-m2m', 'client_id': 'client', 'http_path': '/sql/1.0/warehouses/example'}) + sql = MagicMock() + adapter = DatabricksAdapter() + with patch.object(adapter, '_import_driver_module', return_value=sql), patch( + 'sqlit.domains.connections.providers.databricks.adapter._build_m2m_credentials_provider', return_value='provider' + ) as factory: + adapter.connect(cfg) + factory.assert_called_once_with('host', 'client', 'SECRET') + assert sql.connect.call_args.kwargs['credentials_provider'] == 'provider' + + +@pytest.mark.parametrize(('method', 'table_type'), [('get_tables', 'TABLE'), ('get_views', 'VIEW')]) +def test_hive_catalog_uses_connector_metadata_instead_of_information_schema(method, table_type): + conn = MagicMock() + cursor = conn.cursor.return_value + cursor.fetchall.return_value = [SimpleNamespace(TABLE_CAT='hive_metastore', TABLE_SCHEM='demo', TABLE_NAME='example')] + assert getattr(DatabricksAdapter(), method)(conn, 'hive_metastore') == [('demo', 'example')] + cursor.tables.assert_called_once_with(catalog_name='hive_metastore', table_types=[table_type]) + cursor.execute.assert_not_called() + cursor.close.assert_called_once() + + +def test_hive_column_metadata_filters_wildcard_matches(): + conn = MagicMock() + cursor = conn.cursor.return_value + cursor.fetchall.return_value = [ + SimpleNamespace(TABLE_CAT='hive_metastore', TABLE_SCHEM='demo', TABLE_NAME='a_b', COLUMN_NAME='correct', TYPE_NAME='INT'), + SimpleNamespace(TABLE_CAT='hive_metastore', TABLE_SCHEM='demo', TABLE_NAME='axb', COLUMN_NAME='wrong', TYPE_NAME='INT'), + ] + columns = DatabricksAdapter().get_columns(conn, 'a_b', 'hive_metastore', 'demo') + assert [c.name for c in columns] == ['correct'] + cursor.columns.assert_called_once_with(catalog_name='hive_metastore', schema_name='demo', table_name='a_b') + cursor.execute.assert_not_called() + cursor.close.assert_called_once() + + +def test_m2m_factory_builds_valid_sdk_configuration(): + sdk = pytest.importorskip('databricks.sdk.core') + from sqlit.domains.connections.providers.databricks.adapter import _build_m2m_credentials_provider + + def headers(): + return {'Authorization': 'Bearer SYNTHETIC_ACCESS'} + + class StaticCredentials: + def __call__(self, config): + return headers + + def auth_type(self): + return 'test' + + real_config = sdk.Config + with ( + patch.object(real_config, '_resolve_host_metadata', return_value=None, create=True), + patch.object(sdk, 'Config', side_effect=lambda **kwargs: real_config(credentials_strategy=StaticCredentials(), **kwargs)), + patch.object(sdk, 'oauth_service_principal', return_value=headers) as authenticate, + ): + factory = _build_m2m_credentials_provider('example.invalid', 'client', 'SYNTHETIC_SECRET') + assert factory() is headers + config = authenticate.call_args.args[0] + assert config.host == 'https://example.invalid' + assert config.client_id == 'client' + assert config.client_secret == 'SYNTHETIC_SECRET' diff --git a/uv.lock b/uv.lock index efa0b1a3..f8b4b1ad 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -938,6 +946,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] +[[package]] +name = "databricks-sdk" +version = "0.135.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "protobuf" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/7f/3751f29e0540265fdb867074d53af2843b27b773690e34486cc0d3254119/databricks_sdk-0.135.0.tar.gz", hash = "sha256:05c5c6b4378640eebf4f451342d5cceac78aa986a866d06e7901c48169005e12", size = 1091385, upload-time = "2026-09-04T04:34:34.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/8f/0c8bb54125f35aa72b5e6f4b4eb4cf78f2e47fbee38c506967a02bf2db42/databricks_sdk-0.135.0-py3-none-any.whl", hash = "sha256:97bc7f4f969e54e7a40946f20c8de5df1c7f3e384520eba5dcf4beeab631ecae", size = 1034820, upload-time = "2026-09-04T04:34:33.045Z" }, +] + +[[package]] +name = "databricks-sql-connector" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lz4" }, + { name = "oauthlib" }, + { name = "openpyxl" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pybreaker" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "thrift" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/01/7cc496468e38485b5577eec978a72572d9631b15945912cb9f51fd9db241/databricks_sql_connector-4.5.0.tar.gz", hash = "sha256:69f7ac2cc35f77f78c62ffb1aa09dda3aeec3c5037b9d7eddca78ee2e81f046e", size = 239485, upload-time = "2026-09-01T20:08:50.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a2/7bd3b6f494fc68d4b54bc7ca41061efee078dc4d0ecbadcee2b05dfbfd1c/databricks_sql_connector-4.5.0-py3-none-any.whl", hash = "sha256:2b73a5e3688621873cec90ef27ca367ad30a28a8c03c65472638003440a5408c", size = 265151, upload-time = "2026-09-01T20:08:49.419Z" }, +] + [[package]] name = "decorator" version = "5.3.1" @@ -1012,6 +1057,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/5a/8af5b96ce5622b6168854f479ce846cf7fb589813dcc7d8724233c37ded3/duckdb-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:90f241f25cffe7241bf9f376754a5845c74775e00e1c5731119dc88cd71e0cb2", size = 13527759, upload-time = "2025-12-09T10:59:05.496Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -1599,17 +1653,17 @@ wheels = [ [[package]] name = "impyla" -version = "0.22.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bitarray" }, - { name = "six" }, + { name = "pure-sasl" }, { name = "thrift" }, { name = "thrift-sasl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/34/894799eb934954188079a82a68085ae757bbac2496e0a52eebb0e5210592/impyla-0.22.0.tar.gz", hash = "sha256:19def919ef8295a622fdf2f6d04fe1e5954c4d932a397fbde26a89c6c29ef33d", size = 276105, upload-time = "2025-07-31T09:02:50.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/43/906b66fda1b83be56928a63784fa8c5bedb8ed48efa8f74cf9a7f498f685/impyla-0.24.0.tar.gz", hash = "sha256:7c0ee0579aab4cbf1a0c1278ed06acdc982369fad0df7d673a009f59d0e04b05", size = 329003, upload-time = "2026-06-19T13:37:40.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a3/f32c2f6831e56f10727eb9233a822e1b9438f4cd6e1595539432ed1f9196/impyla-0.22.0-py2.py3-none-any.whl", hash = "sha256:bb27777dcd712ea2f0267338e0c925dade7d8fa3b1436e49097ed37ef3a90c92", size = 310167, upload-time = "2025-07-31T09:02:48.321Z" }, + { url = "https://files.pythonhosted.org/packages/27/3d/1fd8f98777df54a8e181c4beb6bd0fbdbc631093c0337986c8fc4c853ddf/impyla-0.24.0-py3-none-any.whl", hash = "sha256:48f0dcd727af111212162bcf55dda3a545b22d862294ed19a973b47f23e800e2", size = 372609, upload-time = "2026-06-19T13:37:39.14Z" }, ] [[package]] @@ -2390,6 +2444,260 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.41.0" @@ -2586,6 +2894,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { 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/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + [[package]] name = "paramiko" version = "3.5.1" @@ -2881,9 +3322,15 @@ name = "pyarrow" version = "21.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } @@ -2930,7 +3377,9 @@ name = "pyarrow" version = "22.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } wheels = [ @@ -3022,6 +3471,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/55/0cf7be399d2cb4eb36399e4ba3defdd89b1c25831c26b8c6d6bb136c49fa/pyathena-3.22.0-py3-none-any.whl", hash = "sha256:a60d25e8918833218f952606d4fc96ab25623eff550415e245b58896f145d6c8", size = 112248, upload-time = "2025-11-17T13:52:31.171Z" }, ] +[[package]] +name = "pybreaker" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/89/fbf98e383f1ec6d117af2cd983efdb3eb7018b63834c427025764194cac2/pybreaker-1.4.1.tar.gz", hash = "sha256:8df2d245c73ba40c8242c56ffb4f12138fbadc23e296224740c2028ea9dc1178", size = 15555, upload-time = "2025-09-21T15:12:04.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/75/e64d3d40a741e2be21d69154f4e5c43a66f0c603c5ef11f49e01429a5932/pybreaker-1.4.1-py3-none-any.whl", hash = "sha256:b4dab4a05195b7f2a64a6c1a6c4ba7a96534ef56ea7210e6bcb59f28897160e0", size = 12915, upload-time = "2025-09-21T15:12:02.284Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -3745,6 +4203,8 @@ dependencies = [ all = [ { name = "adbc-driver-flightsql" }, { name = "clickhouse-connect" }, + { name = "databricks-sdk" }, + { name = "databricks-sql-connector" }, { name = "duckdb" }, { name = "firebirdsql" }, { name = "google-cloud-bigquery" }, @@ -3784,6 +4244,10 @@ cockroachdb = [ d1 = [ { name = "requests" }, ] +databricks = [ + { name = "databricks-sdk" }, + { name = "databricks-sql-connector" }, +] db2 = [ { name = "ibm-db" }, ] @@ -3883,6 +4347,10 @@ requires-dist = [ { name = "adbc-driver-flightsql", marker = "extra == 'flight'", specifier = ">=1.0.0" }, { name = "clickhouse-connect", marker = "extra == 'all'", specifier = ">=0.7.0" }, { name = "clickhouse-connect", marker = "extra == 'clickhouse'", specifier = ">=0.7.0" }, + { name = "databricks-sdk", marker = "extra == 'all'", specifier = ">=0.18.0" }, + { name = "databricks-sdk", marker = "extra == 'databricks'", specifier = ">=0.18.0" }, + { name = "databricks-sql-connector", marker = "extra == 'all'", specifier = ">=3.0.0" }, + { name = "databricks-sql-connector", marker = "extra == 'databricks'", specifier = ">=3.0.0" }, { name = "docker", specifier = ">=7.0.0" }, { name = "duckdb", marker = "extra == 'all'", specifier = ">=1.1.0" }, { name = "duckdb", marker = "extra == 'duckdb'", specifier = ">=1.1.0" }, @@ -3940,7 +4408,7 @@ requires-dist = [ { name = "trino", extras = ["gssapi"], marker = "extra == 'trino-gssapi'", specifier = ">=0.329.0" }, { name = "trino", extras = ["kerberos"], marker = "extra == 'trino-kerberos'", specifier = ">=0.329.0" }, ] -provides-extras = ["all", "athena", "bigquery", "clickhouse", "cockroachdb", "d1", "db2", "duckdb", "firebird", "flight", "hana", "impala", "mariadb", "mssql", "mysql", "oracle", "osquery", "postgres", "presto", "redshift", "snowflake", "spanner", "ssh", "surrealdb", "teradata", "trino", "trino-gssapi", "trino-kerberos", "turso"] +provides-extras = ["all", "athena", "bigquery", "clickhouse", "cockroachdb", "d1", "databricks", "db2", "duckdb", "firebird", "flight", "hana", "impala", "mariadb", "mssql", "mysql", "oracle", "osquery", "postgres", "presto", "redshift", "snowflake", "spanner", "ssh", "surrealdb", "teradata", "trino", "trino-gssapi", "trino-kerberos", "turso"] [package.metadata.requires-dev] dev = [ @@ -4097,12 +4565,46 @@ wheels = [ [[package]] name = "thrift" -version = "0.16.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, +sdist = { url = "https://files.pythonhosted.org/packages/5f/bd/8f90501b11206e545da3343ed0e5740fc694be24a5f637d7dd7e4e3af927/thrift-0.24.0.tar.gz", hash = "sha256:9ef601c49e988475ff0e741d8e1b45feec23b48514e524341efc274191f1789c", size = 69228, upload-time = "2026-07-11T16:53:50.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/0b/5a7e760e711a4103557855613ae929c47a3949852cbcb403b7c21e0848fc/thrift-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efe85c4508adaf6c9e6f7fac2b1c3c9beb4b39b19c375519966335a70b28e64a", size = 184200, upload-time = "2026-07-11T16:53:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/1af719a59c1e75ff25a8f917c4111e7f9c17f2a58840d44f697e11a8066d/thrift-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2baaec0c5cd7ba3eace54b26d81fdf0f5a85010468687cf4a131871d65abfed", size = 183616, upload-time = "2026-07-11T16:53:06.578Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/5ec88cb4e7f0818e5048bb50c36a342675a40f381d8f53a62f588fc71de5/thrift-0.24.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cad27a826e86739a79a327e6afae5a9bea36aeed2989c4318fc2943b6cfad095", size = 488814, upload-time = "2026-07-11T16:53:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/18/e7/3eb3b653f3eff1d14fe3c3ed17cab8d8ae1a0bd44f486fe7c667afe88f5d/thrift-0.24.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e26ca0f346e5ec2b6be9778573fb2e9d8b3eb162dbce94e61906176158b448b", size = 494575, upload-time = "2026-07-11T16:53:09.013Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/d55245458f990b4f440b71012f355d0cdcac294d7d949d1340c42973826e/thrift-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:498ae392090d7ae17ab59ebd8dfabda76528eeb22d3890a57c9f226003d7ed6e", size = 1434147, upload-time = "2026-07-11T16:53:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/505418d1797ba7940b5ea65d96e88c7e702ab2f3cbc64007c3f14f868aa5/thrift-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:acbd02baacfa0d2017f85b189ed69cd6baa522785d1ad86476eb7ce8cd16d063", size = 1492591, upload-time = "2026-07-11T16:53:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/882c647a219ff036fb73bf51cec9e1f432e34a687cb92f747ff51ab7d7c4/thrift-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:887a10d718d85275da70fe4e9d4740268ae2854f205fb5869909d24cc5576b79", size = 369174, upload-time = "2026-07-11T16:53:13.287Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/7ba9f07a7ac7b98dd7f29f2d9e6f6e98e332942dc8dbd82b71abbac1233d/thrift-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:873c93d8496cf71589efd3128ddc5af0be350e6c1a0e615475d840eaa54f6124", size = 228606, upload-time = "2026-07-11T16:53:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/4d0d03b40063504e75c9a74456dbca097ee78af92ddfae6c0216da9d2717/thrift-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:048d768e5822c436e8920b987d89a6a26e4d438ddf2de4b9d3f81444cd03cd04", size = 227999, upload-time = "2026-07-11T16:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/74758d030d8d1f103f32026b92e0c9c8b14a6d190197a5e8055b9697294d/thrift-0.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2592bb0bf232d0808583626a7a8d71e265851e974c182967d67dde1f16902aee", size = 535383, upload-time = "2026-07-11T16:53:17.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/d5/2f1e521dd0e8525c460e6e2ed3938f56b0d8208d794e08e0309f6e2ae299/thrift-0.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca1b1ba00151565dc4e6673c924debd557aaf25b2790f1570b3430cb35503671", size = 540968, upload-time = "2026-07-11T16:53:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/44abc539a6356d8ffc47fd36724b376d4d4d904924a720b4416911cd8009/thrift-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:afb7977ccb8ecd7b1520a5141de64b58e94712528a26c10ac8f5b4ef220112a5", size = 1480581, upload-time = "2026-07-11T16:53:19.772Z" }, + { url = "https://files.pythonhosted.org/packages/34/bc/77a6c2f8f4983473a22905d7c420eb6d6ea46ee590bc287f3443533d0525/thrift-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c8d37a4311a87bda4bd2b8e4137eec638c18e3a57d11a1011bd98a8867523860", size = 1538742, upload-time = "2026-07-11T16:53:21.216Z" }, + { url = "https://files.pythonhosted.org/packages/06/b1/4c5e8ac71390292a70c4d75d695a562b6d6c081316d37f6121e18e1878d9/thrift-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbf461351940ddaa85bf8c2ee1754c9cfdd33bb78322e635e1ea5cd3947ae49a", size = 413572, upload-time = "2026-07-11T16:53:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/39/f3e6ec283fa6dbd490eab25a33c46edb6b3114ff9dc46b31bd88af3fd4ea/thrift-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:306e0fe3c96b300471c19cec60bd6816064fd93d04713488ef07120aff84653b", size = 223061, upload-time = "2026-07-11T16:53:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/45/86/efd51714e5ce62c4d3c7f998eb1b1f5526cd1ee9fb4080545e7fc25fe5cf/thrift-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ac42199ea2a6fc6275b0aeec32ae09e7f9edb43890ff9a1af5e34191fb422cc", size = 222518, upload-time = "2026-07-11T16:53:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/4ea971750d449180363aa2273f1d80e66ce068643c5993589978d7a3951f/thrift-0.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cdd5f927eb98d0e8f00ab148b0cb66ae2598a396e907bd2b6febcbdcc46d80c", size = 531068, upload-time = "2026-07-11T16:53:26.044Z" }, + { url = "https://files.pythonhosted.org/packages/97/d8/cbd6352723ab4a6668c40c0d10a680b0f05178df2de9ea919ea8e8d2b834/thrift-0.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:134cd1a0d80f928377808348d1f9f647284ada093573beaa47040bed5507e219", size = 539609, upload-time = "2026-07-11T16:53:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/af2e258f9c135dc359acda9390e557ddc501a9185b65892fc253f94ca160/thrift-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:518199062feabfc0982767a0f7bceccb4fd1b485654f75e02913837444c3c130", size = 1474702, upload-time = "2026-07-11T16:53:28.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/93424dc6ceb2a561d055df4fee825ff7116e342e126c6aab357e0a94185e/thrift-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8c5de86af2a44641d423624c71c554a691d9f80c8b87709f15c7f98ccc6aeac", size = 1535581, upload-time = "2026-07-11T16:53:29.967Z" }, + { url = "https://files.pythonhosted.org/packages/c5/35/88debea28426b31ac417a83cda5c029f67cf72665fd2327562adb5c151b1/thrift-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f82bb16c4f2009dbc4fc9374604f05e998fb33be6a7787e095cea9842ecfa1d", size = 407669, upload-time = "2026-07-11T16:53:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/93/5a/3b0d7b47ae73c28891997433447f87ba26de0fc11ca9fd4ea8c6b497bf23/thrift-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb8b389142d67554d0de814dd6c1d62b962751f68721537efca429c57b09327", size = 224851, upload-time = "2026-07-11T16:53:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/4e17cce911ebbf76c59472b488dbe25c32d51cef2500b6e255a9fb59ff02/thrift-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:18e7693f1bcef45937ad31a02564add55dec754710d42afc8ee3fc26ecf13028", size = 224313, upload-time = "2026-07-11T16:53:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/eb8cb5a75fdff60b21b78c712a455adac01e1768792c3232184fa7202961/thrift-0.24.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937b398c311799fd5eddaeaffbd275510c68ead597eb1897e5e8113542fb2b50", size = 532517, upload-time = "2026-07-11T16:53:35.458Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/d0ecdf3573eb4026343d04efa64cf465a8453e699bd41a2d68156c3caf83/thrift-0.24.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b89a83d4e9ae6029e14f6f7c73305044bf02739d729c6aa8025ef3b6faef0ec0", size = 541124, upload-time = "2026-07-11T16:53:36.767Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/37daa851995ad441e38f83b2a8e67cdf68df10148779fb5bbafd4fb24f6b/thrift-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6381093f2c54e0f108554004a8cac3f1ef7ba99d6c5a9909c0eb64f450142685", size = 1476528, upload-time = "2026-07-11T16:53:38.056Z" }, + { url = "https://files.pythonhosted.org/packages/d6/67/80f3aeced7a38d17a281d1b9904ab9e93ab493f5c75e144b3180c278041c/thrift-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28e034658d724aaa66007babb258ef12f0294036812fc449d7ce6fd15b07100f", size = 1537515, upload-time = "2026-07-11T16:53:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/169f93d531a0d9546b6f18dacf37752c27cccfa719bf231162d1fcf8d0e0/thrift-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:f4a44391ae1e32817553639b2991b0753d84487259fe87f8111c956c6bdebf43", size = 409450, upload-time = "2026-07-11T16:53:40.711Z" }, + { url = "https://files.pythonhosted.org/packages/01/46/e229eec0531070a76f2d98335f57ec29df5fb991d7a00e8b5aac2064e893/thrift-0.24.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7beb05268c76895f7c3130ce747a8a8cf35558fbc7f464f994e20a0c92181cbd", size = 227137, upload-time = "2026-07-11T16:53:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/6e/30/27b0702a183de1fc884622a584093756eba6eb33a2a21760b279ff124573/thrift-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f40bdfbaf8d2795b1593bb75b4d9c77831bbe96a1ef59da4634ccc125a17dca5", size = 226617, upload-time = "2026-07-11T16:53:43.135Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/aa45a1d88c3692c14420095a3d3631c6add18e3c33527bdf5adfcaed5f1e/thrift-0.24.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:272624d36faa7bee01b94e5da5efc7305d9d3da57d005382c93fcbde8c3edf6a", size = 534129, upload-time = "2026-07-11T16:53:44.214Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4f/b500ce0d9cf29a83cab88ab460ce7a7bab955f2a5d30bb1e7a9d4d4c2242/thrift-0.24.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bc91ae93005bc16c7362edcb58818a547850b9b52fe9b8075cc6b8922bcde2d", size = 543379, upload-time = "2026-07-11T16:53:45.394Z" }, + { url = "https://files.pythonhosted.org/packages/32/77/0ecb68cf0ea0af2c51ba88cf7f85ce59823efc7a7cb7573abaac0d60d49b/thrift-0.24.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c05bb4eac921836c12cdf30c237283df8a42aff6540f7970cd72c5959b139970", size = 1478854, upload-time = "2026-07-11T16:53:46.654Z" }, + { url = "https://files.pythonhosted.org/packages/fa/89/fd2a0b7ef3a54bac545fdaf9e34b3d3012471058a9eddbe5a2697bf69773/thrift-0.24.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e5da6b391828d46df9471f22181374760a91773b7e07ad7eb44f351c90580662", size = 1539741, upload-time = "2026-07-11T16:53:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/a5/10/7b3368202c2333d448fa9b13288ecb8f68e9361531c8757a7b1eb27ab750/thrift-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:829db909a053d4064cb9fe574cc1f5994c24d727f61cdf6446ca774cd3ba5268", size = 419738, upload-time = "2026-07-11T16:53:49.773Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/23/dd951c9883cb49a73b750bdfe91e39d78e8a3f1f7175608634f381a197d5/thrift-0.16.0.tar.gz", hash = "sha256:2b5b6488fcded21f9d312aa23c9ff6a0195d0f6ae26ddbd5ad9e3e25dfc14408", size = 59605, upload-time = "2022-03-31T14:54:06.866Z" } [[package]] name = "thrift-sasl"