From 4c83730ab17b1334baef1022bcc3bacc2755a94f Mon Sep 17 00:00:00 2001 From: Judah Rand <17158624+judahrand@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:13:34 +0000 Subject: [PATCH 1/2] Use `psycopg` rather than `psycopg2` for Postgres --- .github/renovate.json | 4 +- conda/environment-arm64-flink.yml | 2 +- conda/environment-arm64.yml | 2 +- conda/environment.yml | 2 +- ibis/backends/postgres/__init__.py | 43 +++++++++---------- ibis/backends/postgres/tests/conftest.py | 2 +- ibis/backends/postgres/tests/test_client.py | 8 ++-- .../backends/postgres/tests/test_functions.py | 6 +-- ibis/backends/postgres/tests/test_postgis.py | 2 +- ibis/backends/postgres/tests/test_udf.py | 2 +- ibis/backends/tests/errors.py | 19 ++++++++ ibis/backends/tests/test_array.py | 27 ++++++++---- ibis/backends/tests/test_client.py | 4 +- ibis/backends/tests/test_generic.py | 4 +- ibis/backends/tests/test_numeric.py | 4 +- ibis/backends/tests/test_struct.py | 8 ++-- pyproject.toml | 2 +- requirements-dev.txt | 1 + uv.lock | 18 +++++++- 19 files changed, 102 insertions(+), 58 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 39d7990ade72..6fbdba515037 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -69,7 +69,7 @@ }, { "addLabels": ["postgres"], - "matchPackageNames": ["/psycopg2/", "/postgres/"] + "matchPackageNames": ["/psycopg/", "/postgres/"] }, { "addLabels": ["druid"], @@ -89,7 +89,7 @@ }, { "addLabels": ["risingwave"], - "matchPackageNames": ["/risingwave/"] + "matchPackageNames": ["/psycopg2/", "/risingwave/"] }, { "addLabels": ["snowflake"], diff --git a/conda/environment-arm64-flink.yml b/conda/environment-arm64-flink.yml index 5678f58e6e4c..fca9fee7e01c 100644 --- a/conda/environment-arm64-flink.yml +++ b/conda/environment-arm64-flink.yml @@ -26,7 +26,7 @@ dependencies: - pins >=0.8.2 - uv>=0.4.29 - polars >=1,<2 - - psycopg2 >=2.8.4 + - psycopg >= 3.2.0 - pyarrow =11.0.0 - pyarrow-tests - pyarrow-hotfix >=0.4 diff --git a/conda/environment-arm64.yml b/conda/environment-arm64.yml index e8082a259351..ec536e4282f0 100644 --- a/conda/environment-arm64.yml +++ b/conda/environment-arm64.yml @@ -26,7 +26,7 @@ dependencies: - pins >=0.8.2 - uv>=0.4.29 - polars >=1,<2 - - psycopg2 >=2.8.4 + - psycopg >= 3.2.0 - pyarrow >=10.0.1 - pyarrow-tests - pyarrow-hotfix >=0.4 diff --git a/conda/environment.yml b/conda/environment.yml index 2b6eb514dec5..1f4b226fd852 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -27,7 +27,7 @@ dependencies: - pip - uv>=0.4.29 - polars >=1,<2 - - psycopg2 >=2.8.4 + - psycopg >= 3.2.0 - pyarrow >=10.0.1 - pyarrow-hotfix >=0.4 - pydata-google-auth diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py index e6c9988f411e..e4e89d9faeba 100644 --- a/ibis/backends/postgres/__init__.py +++ b/ibis/backends/postgres/__init__.py @@ -31,7 +31,7 @@ import pandas as pd import polars as pl - import psycopg2 + import psycopg import pyarrow as pa @@ -90,8 +90,6 @@ def _from_url(self, url: ParseResult, **kwargs): return self.connect(**kwargs) def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: - from psycopg2.extras import execute_batch - schema = op.schema if null_columns := [col for col, dtype in schema.items() if dtype.is_null()]: raise exc.IbisTypeError( @@ -129,7 +127,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: with self.begin() as cur: cur.execute(create_stmt_sql) - execute_batch(cur, sql, data, 128) + cur.executemany(sql, data) @contextlib.contextmanager def begin(self): @@ -145,14 +143,16 @@ def begin(self): finally: cursor.close() - def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame: + def _fetch_from_cursor( + self, cursor: psycopg.Cursor, schema: sch.Schema + ) -> pd.DataFrame: import pandas as pd from ibis.backends.postgres.converter import PostgresPandasData try: df = pd.DataFrame.from_records( - cursor, columns=schema.names, coerce_float=True + cursor.fetchall(), columns=schema.names, coerce_float=True ) except Exception: # clean up the cursor if we fail to create the DataFrame @@ -166,7 +166,7 @@ def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame: @property def version(self): - version = f"{self.con.server_version:0>6}" + version = f"{self.con.info.server_version:0>6}" major = int(version[:2]) minor = int(version[2:4]) patch = int(version[4:]) @@ -233,17 +233,17 @@ def do_connect( year int32 month int32 """ - import psycopg2 - import psycopg2.extras + import psycopg + import psycopg.types.json - psycopg2.extras.register_default_json(loads=lambda x: x) + psycopg.types.json.set_json_loads(loads=lambda x: x) - self.con = psycopg2.connect( + self.con = psycopg.connect( host=host, port=port, user=user, password=password, - database=database, + dbname=database, options=(f"-csearch_path={schema}" * (schema is not None)) or None, **kwargs, ) @@ -252,7 +252,7 @@ def do_connect( @util.experimental @classmethod - def from_connection(cls, con: psycopg2.extensions.connection) -> Backend: + def from_connection(cls, con: psycopg.Connection) -> Backend: """Create an Ibis client from an existing connection to a PostgreSQL database. Parameters @@ -701,8 +701,9 @@ def _safe_raw_sql(self, *args, **kwargs): yield result def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: - import psycopg2 - import psycopg2.extras + import psycopg + import psycopg.types + import psycopg.types.hstore with contextlib.suppress(AttributeError): query = query.sql(dialect=self.dialect) @@ -711,13 +712,11 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: cursor = con.cursor() try: - # try to load hstore, uuid and ipaddress extensions - with contextlib.suppress(psycopg2.ProgrammingError): - psycopg2.extras.register_hstore(cursor) - with contextlib.suppress(psycopg2.ProgrammingError): - psycopg2.extras.register_uuid(conn_or_curs=cursor) - with contextlib.suppress(psycopg2.ProgrammingError): - psycopg2.extras.register_ipaddress(cursor) + # try to load hstore + with contextlib.suppress(TypeError): + type_info = psycopg.types.TypeInfo.fetch(con, "hstore") + with contextlib.suppress(psycopg.ProgrammingError, TypeError): + psycopg.types.hstore.register_hstore(type_info, cursor) except Exception: cursor.close() raise diff --git a/ibis/backends/postgres/tests/conftest.py b/ibis/backends/postgres/tests/conftest.py index 9e20f74c1479..a0ec63dfae20 100644 --- a/ibis/backends/postgres/tests/conftest.py +++ b/ibis/backends/postgres/tests/conftest.py @@ -48,7 +48,7 @@ class TestConf(ServiceBackendTest): supports_structs = False rounding_method = "half_to_even" service_name = "postgres" - deps = ("psycopg2",) + deps = ("psycopg",) driver_supports_multiple_statements = True diff --git a/ibis/backends/postgres/tests/test_client.py b/ibis/backends/postgres/tests/test_client.py index 783fce2f0a78..2f2cd9c6d8e2 100644 --- a/ibis/backends/postgres/tests/test_client.py +++ b/ibis/backends/postgres/tests/test_client.py @@ -30,10 +30,10 @@ import ibis.common.exceptions as com import ibis.expr.datatypes as dt import ibis.expr.types as ir -from ibis.backends.tests.errors import PsycoPg2OperationalError +from ibis.backends.tests.errors import PsycoPgOperationalError from ibis.util import gen_name -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") POSTGRES_TEST_DB = os.environ.get("IBIS_TEST_POSTGRES_DATABASE", "ibis_testing") IBIS_POSTGRES_HOST = os.environ.get("IBIS_TEST_POSTGRES_HOST", "localhost") @@ -260,7 +260,7 @@ def test_kwargs_passthrough_in_connect(): def test_port(): # check that we parse and use the port (and then of course fail cuz it's bogus) - with pytest.raises(PsycoPg2OperationalError): + with pytest.raises(PsycoPgOperationalError): ibis.connect("postgresql://postgres:postgres@localhost:1337/ibis_testing") @@ -388,7 +388,7 @@ def test_password_with_bracket(): quoted_pass = quote_plus(password) url = f"postgres://{IBIS_POSTGRES_USER}:{quoted_pass}@{IBIS_POSTGRES_HOST}:{IBIS_POSTGRES_PORT}/{POSTGRES_TEST_DB}" with pytest.raises( - PsycoPg2OperationalError, + PsycoPgOperationalError, match=f'password authentication failed for user "{IBIS_POSTGRES_USER}"', ): ibis.connect(url) diff --git a/ibis/backends/postgres/tests/test_functions.py b/ibis/backends/postgres/tests/test_functions.py index a07c8d81aeb8..975baf3e6923 100644 --- a/ibis/backends/postgres/tests/test_functions.py +++ b/ibis/backends/postgres/tests/test_functions.py @@ -16,7 +16,7 @@ import ibis.expr.types as ir from ibis import literal as L -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") @pytest.mark.parametrize( @@ -1195,7 +1195,7 @@ def test_string_to_binary_cast(con): ) with con.begin() as c: c.execute(sql_string) - raw_data = [row[0][0] for row in c.fetchall()] + raw_data = [row[0] for row in c.fetchall()] expected = pd.Series(raw_data, name=name) tm.assert_series_equal(result, expected) @@ -1212,7 +1212,7 @@ def test_string_to_binary_round_trip(con): ) with con.begin() as c: c.execute(sql_string) - rows = [row[0] for (row,) in c.fetchall()] + rows = [row[0] for row in c.fetchall()] expected = pd.Series(rows, name=name) tm.assert_series_equal(result, expected) diff --git a/ibis/backends/postgres/tests/test_postgis.py b/ibis/backends/postgres/tests/test_postgis.py index 9a20356a2553..aa8a58b686e3 100644 --- a/ibis/backends/postgres/tests/test_postgis.py +++ b/ibis/backends/postgres/tests/test_postgis.py @@ -7,7 +7,7 @@ import pytest from numpy import testing -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") gpd = pytest.importorskip("geopandas") pytest.importorskip("shapely") diff --git a/ibis/backends/postgres/tests/test_udf.py b/ibis/backends/postgres/tests/test_udf.py index 0c56392c04bf..a680cc4fec66 100644 --- a/ibis/backends/postgres/tests/test_udf.py +++ b/ibis/backends/postgres/tests/test_udf.py @@ -12,7 +12,7 @@ from ibis import udf from ibis.util import guid -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") @pytest.fixture(scope="session") diff --git a/ibis/backends/tests/errors.py b/ibis/backends/tests/errors.py index 716e9796d4b9..53e25e90f170 100644 --- a/ibis/backends/tests/errors.py +++ b/ibis/backends/tests/errors.py @@ -112,6 +112,25 @@ except ImportError: TrinoUserError = None +try: + from psycopg.errors import ArraySubscriptError as PsycoPgArraySubscriptError + from psycopg.errors import DivisionByZero as PsycoPgDivisionByZero + from psycopg.errors import IndeterminateDatatype as PsycoPgIndeterminateDatatype + from psycopg.errors import InternalError_ as PsycoPgInternalError + from psycopg.errors import ( + InvalidTextRepresentation as PsycoPgInvalidTextRepresentation, + ) + from psycopg.errors import OperationalError as PsycoPgOperationalError + from psycopg.errors import ProgrammingError as PsycoPgProgrammingError + from psycopg.errors import SyntaxError as PsycoPgSyntaxError + from psycopg.errors import UndefinedObject as PsycoPgUndefinedObject +except ImportError: + PsycoPgSyntaxError = PsycoPgIndeterminateDatatype = ( + PsycoPgInvalidTextRepresentation + ) = PsycoPgDivisionByZero = PsycoPgInternalError = PsycoPgProgrammingError = ( + PsycoPgOperationalError + ) = PsycoPgUndefinedObject = PsycoPgArraySubscriptError = None + try: from psycopg2.errors import ArraySubscriptError as PsycoPg2ArraySubscriptError from psycopg2.errors import DivisionByZero as PsycoPg2DivisionByZero diff --git a/ibis/backends/tests/test_array.py b/ibis/backends/tests/test_array.py index 9e8d9dfde29f..d47f2d7e085a 100644 --- a/ibis/backends/tests/test_array.py +++ b/ibis/backends/tests/test_array.py @@ -22,11 +22,13 @@ GoogleBadRequest, MySQLOperationalError, PolarsComputeError, - PsycoPg2ArraySubscriptError, PsycoPg2IndeterminateDatatype, PsycoPg2InternalError, PsycoPg2ProgrammingError, - PsycoPg2SyntaxError, + PsycoPgIndeterminateDatatype, + PsycoPgInternalError, + PsycoPgInvalidTextRepresentation, + PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, PyAthenaOperationalError, @@ -1094,7 +1096,7 @@ def test_array_intersect(con, data): @builtin_array -@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError) +@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) @pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError) @pytest.mark.notimpl( ["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError @@ -1114,7 +1116,7 @@ def test_unnest_struct(con): @builtin_array -@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError) +@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) @pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError) @pytest.mark.notimpl( ["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError @@ -1205,7 +1207,7 @@ def test_zip_null(con, fn): @builtin_array -@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError) +@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) @pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError) @pytest.mark.notimpl(["datafusion"], raises=Exception, reason="not yet supported") @pytest.mark.notimpl( @@ -1276,8 +1278,17 @@ def flatten_data(): ["bigquery"], reason="BigQuery doesn't support arrays of arrays", raises=TypeError ) @pytest.mark.notyet( - ["postgres", "risingwave"], + ["postgres"], reason="Postgres doesn't truly support arrays of arrays", + raises=( + com.OperationNotDefinedError, + PsycoPgIndeterminateDatatype, + PsycoPgInternalError, + ), +) +@pytest.mark.notyet( + ["risingwave"], + reason="Risingwave doesn't truly support arrays of arrays", raises=( com.OperationNotDefinedError, PsycoPg2IndeterminateDatatype, @@ -1769,7 +1780,7 @@ def test_table_unnest_column_expr(backend): ) @pytest.mark.notimpl(["trino"], raises=TrinoUserError) @pytest.mark.notimpl(["athena"], raises=PyAthenaOperationalError) -@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError) +@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) @pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError) @pytest.mark.notyet( ["risingwave"], raises=PsycoPg2InternalError, reason="not supported in risingwave" @@ -1887,7 +1898,7 @@ def test_array_agg_bool(con, data, agg, baseline_func): @pytest.mark.notyet( ["postgres"], - raises=PsycoPg2ArraySubscriptError, + raises=PsycoPgInvalidTextRepresentation, reason="all dimensions must match in size", ) @pytest.mark.notimpl(["risingwave", "flink"], raises=com.OperationNotDefinedError) diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index 28b701410831..10c87bcd592a 100644 --- a/ibis/backends/tests/test_client.py +++ b/ibis/backends/tests/test_client.py @@ -32,7 +32,7 @@ ImpalaHiveServer2Error, OracleDatabaseError, PsycoPg2InternalError, - PsycoPg2UndefinedObject, + PsycoPgUndefinedObject, Py4JJavaError, PyAthenaDatabaseError, PyODBCProgrammingError, @@ -725,7 +725,7 @@ def test_list_database_contents(con): @pytest.mark.notyet(["databricks"], raises=DatabricksServerOperationError) @pytest.mark.notyet(["bigquery"], raises=com.UnsupportedBackendType) @pytest.mark.notyet( - ["postgres"], raises=PsycoPg2UndefinedObject, reason="no unsigned int types" + ["postgres"], raises=PsycoPgUndefinedObject, reason="no unsigned int types" ) @pytest.mark.notyet( ["oracle"], raises=OracleDatabaseError, reason="no unsigned int types" diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py index ef430953d82c..267a0bc0e5ed 100644 --- a/ibis/backends/tests/test_generic.py +++ b/ibis/backends/tests/test_generic.py @@ -25,7 +25,7 @@ OracleDatabaseError, PolarsInvalidOperationError, PsycoPg2InternalError, - PsycoPg2SyntaxError, + PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, PyAthenaOperationalError, @@ -1739,7 +1739,7 @@ def hash_256(col): pytest.mark.notimpl(["flink"], raises=Py4JJavaError), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl(["oracle"], raises=OracleDatabaseError), - pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError), + pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError), pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError), pytest.mark.notimpl(["snowflake"], raises=AssertionError), pytest.mark.never( diff --git a/ibis/backends/tests/test_numeric.py b/ibis/backends/tests/test_numeric.py index ddca3a4c97b6..a8135b084122 100644 --- a/ibis/backends/tests/test_numeric.py +++ b/ibis/backends/tests/test_numeric.py @@ -22,8 +22,8 @@ ImpalaHiveServer2Error, MySQLOperationalError, OracleDatabaseError, - PsycoPg2DivisionByZero, PsycoPg2InternalError, + PsycoPgDivisionByZero, Py4JError, Py4JJavaError, PyAthenaOperationalError, @@ -1323,7 +1323,7 @@ def test_floating_mod(backend, alltypes, df): ) @pytest.mark.notyet(["mssql"], raises=PyODBCDataError) @pytest.mark.notyet(["snowflake"], raises=SnowflakeProgrammingError) -@pytest.mark.notyet(["postgres"], raises=PsycoPg2DivisionByZero) +@pytest.mark.notyet(["postgres"], raises=PsycoPgDivisionByZero) @pytest.mark.notimpl(["exasol"], raises=ExaQueryError) @pytest.mark.xfail_version(duckdb=["duckdb<1.1"]) def test_divide_by_zero(backend, alltypes, df, column, denominator): diff --git a/ibis/backends/tests/test_struct.py b/ibis/backends/tests/test_struct.py index 5801b97e52ce..48e046cb3a54 100644 --- a/ibis/backends/tests/test_struct.py +++ b/ibis/backends/tests/test_struct.py @@ -13,7 +13,7 @@ DatabricksServerOperationError, PolarsColumnNotFoundError, PsycoPg2InternalError, - PsycoPg2SyntaxError, + PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, PyAthenaOperationalError, @@ -138,7 +138,7 @@ def test_collect_into_struct(alltypes): @pytest.mark.notimpl( - ["postgres"], reason="struct literals not implemented", raises=PsycoPg2SyntaxError + ["postgres"], reason="struct literals not implemented", raises=PsycoPgSyntaxError ) @pytest.mark.notimpl( ["risingwave"], @@ -155,7 +155,7 @@ def test_field_access_after_case(con): @pytest.mark.notimpl( - ["postgres"], reason="struct literals not implemented", raises=PsycoPg2SyntaxError + ["postgres"], reason="struct literals not implemented", raises=PsycoPgSyntaxError ) @pytest.mark.notimpl(["flink"], raises=IbisError, reason="not implemented in ibis") @pytest.mark.parametrize( @@ -242,7 +242,7 @@ def test_keyword_fields(con, nullable): @pytest.mark.notyet( ["postgres"], - raises=PsycoPg2SyntaxError, + raises=PsycoPgSyntaxError, reason="sqlglot doesn't implement structs for postgres correctly", ) @pytest.mark.notyet( diff --git a/pyproject.toml b/pyproject.toml index 91842267eebd..91f1a5918ca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,7 +163,7 @@ polars = [ "rich>=12.4.4,<14", ] postgres = [ - "psycopg2>=2.8.4,<3", + "psycopg>=3.2.0,<4", "pyarrow>=10.0.1", "pyarrow-hotfix>=0.4,<1", "numpy>=1.23.2,<3", diff --git a/requirements-dev.txt b/requirements-dev.txt index 0b23032ca446..9d41bb06c2b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -178,6 +178,7 @@ propcache==0.2.1 proto-plus==1.25.0 protobuf==5.29.2 psutil==6.1.1 +psycopg==3.2.3 psycopg2==2.9.10 psygnal==0.11.1 ptyprocess==0.7.0 ; os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') diff --git a/uv.lock b/uv.lock index e442ff96203d..ef384bef6942 100644 --- a/uv.lock +++ b/uv.lock @@ -2142,7 +2142,7 @@ polars = [ postgres = [ { name = "numpy" }, { name = "pandas" }, - { name = "psycopg2" }, + { name = "psycopg" }, { name = "pyarrow" }, { name = "pyarrow-hotfix" }, { name = "rich" }, @@ -2314,7 +2314,7 @@ requires-dist = [ { name = "parsy", specifier = ">=2" }, { name = "pins", extras = ["gcs"], marker = "extra == 'examples'", specifier = ">=0.8.3,<1" }, { name = "polars", marker = "extra == 'polars'", specifier = ">=1,<2" }, - { name = "psycopg2", marker = "extra == 'postgres'", specifier = ">=2.8.4,<3" }, + { name = "psycopg", marker = "extra == 'postgres'", specifier = ">=3.2.0,<4" }, { name = "psycopg2", marker = "extra == 'risingwave'", specifier = ">=2.8.4,<3" }, { name = "pyarrow", marker = "extra == 'athena'", specifier = ">=10.0.1" }, { name = "pyarrow", marker = "extra == 'bigquery'", specifier = ">=10.0.1" }, @@ -4003,6 +4003,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, ] +[[package]] +name = "psycopg" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/ad/7ce016ae63e231575df0498d2395d15f005f05e32d3a2d439038e1bd0851/psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2", size = 155550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/21/534b8f5bd9734b7a2fcd3a16b1ee82ef6cad81a4796e95ebf4e0c6a24119/psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", size = 197934 }, +] + [[package]] name = "psycopg2" version = "2.9.10" @@ -4015,6 +4028,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/39/5a9a229bb5414abeb86e33b8fc8143ab0aecce5a7f698a53e31367d30caa/psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4", size = 1163736 }, { url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 }, { url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 }, + { url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 }, ] [[package]] From 41c5082c81a8dca47e060fc2a862f84931462fd1 Mon Sep 17 00:00:00 2001 From: Judah Rand <17158624+judahrand@users.noreply.github.com> Date: Thu, 9 Jan 2025 09:38:32 +0000 Subject: [PATCH 2/2] Use `psycopg` rather than `psycopg2` for Risingwave --- .github/renovate.json | 2 +- ibis/backends/postgres/__init__.py | 10 ++--- ibis/backends/risingwave/__init__.py | 9 ++-- ibis/backends/risingwave/tests/conftest.py | 2 +- ibis/backends/risingwave/tests/test_client.py | 2 +- .../risingwave/tests/test_functions.py | 2 +- ibis/backends/tests/errors.py | 19 -------- ibis/backends/tests/test_aggregation.py | 12 ++--- ibis/backends/tests/test_array.py | 44 +++++++++---------- ibis/backends/tests/test_client.py | 12 ++--- ibis/backends/tests/test_generic.py | 10 ++--- ibis/backends/tests/test_map.py | 12 ++--- ibis/backends/tests/test_numeric.py | 12 ++--- ibis/backends/tests/test_param.py | 4 +- ibis/backends/tests/test_set_ops.py | 8 ++-- ibis/backends/tests/test_string.py | 8 ++-- ibis/backends/tests/test_struct.py | 8 ++-- ibis/backends/tests/test_temporal.py | 16 +++---- ibis/backends/tests/test_window.py | 38 ++++++++-------- pyproject.toml | 2 +- requirements-dev.txt | 1 - uv.lock | 19 +------- 22 files changed, 107 insertions(+), 145 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 6fbdba515037..9f1d4036dfea 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -89,7 +89,7 @@ }, { "addLabels": ["risingwave"], - "matchPackageNames": ["/psycopg2/", "/risingwave/"] + "matchPackageNames": ["/psycopg/", "/risingwave/"] }, { "addLabels": ["snowflake"], diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py index e4e89d9faeba..569ebbba10f3 100644 --- a/ibis/backends/postgres/__init__.py +++ b/ibis/backends/postgres/__init__.py @@ -713,11 +713,11 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: try: # try to load hstore - with contextlib.suppress(TypeError): - type_info = psycopg.types.TypeInfo.fetch(con, "hstore") - with contextlib.suppress(psycopg.ProgrammingError, TypeError): - psycopg.types.hstore.register_hstore(type_info, cursor) - except Exception: + psycopg.types.hstore.register_hstore( + psycopg.types.TypeInfo.fetch(con, "hstore"), + cursor, + ) + except (psycopg.InternalError, psycopg.ProgrammingError, TypeError): cursor.close() raise diff --git a/ibis/backends/risingwave/__init__.py b/ibis/backends/risingwave/__init__.py index fd3400fb79a1..0020f154f9a5 100644 --- a/ibis/backends/risingwave/__init__.py +++ b/ibis/backends/risingwave/__init__.py @@ -5,10 +5,9 @@ from operator import itemgetter from typing import TYPE_CHECKING -import psycopg2 +import psycopg import sqlglot as sg import sqlglot.expressions as sge -from psycopg2 import extras import ibis import ibis.backends.sql.compilers as sc @@ -110,12 +109,12 @@ def do_connect( month int32 """ - self.con = psycopg2.connect( + self.con = psycopg.connect( host=host, port=port, user=user, password=password, - database=database, + dbname=database, options=(f"-csearch_path={schema}" * (schema is not None)) or None, ) @@ -289,7 +288,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: ) with self.begin() as cur: cur.execute(create_stmt_sql) - extras.execute_batch(cur, sql, data, 128) + cur.executemany(sql, data) def list_databases( self, *, like: str | None = None, catalog: str | None = None diff --git a/ibis/backends/risingwave/tests/conftest.py b/ibis/backends/risingwave/tests/conftest.py index 06940a6573b6..2832cddedab2 100644 --- a/ibis/backends/risingwave/tests/conftest.py +++ b/ibis/backends/risingwave/tests/conftest.py @@ -33,7 +33,7 @@ class TestConf(ServiceBackendTest): supports_structs = False rounding_method = "half_to_even" service_name = "risingwave" - deps = ("psycopg2",) + deps = ("psycopg",) @property def test_files(self) -> Iterable[Path]: diff --git a/ibis/backends/risingwave/tests/test_client.py b/ibis/backends/risingwave/tests/test_client.py index 1d2ce761242d..c0533a768e17 100644 --- a/ibis/backends/risingwave/tests/test_client.py +++ b/ibis/backends/risingwave/tests/test_client.py @@ -12,7 +12,7 @@ import ibis.expr.types as ir from ibis.util import gen_name -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") RISINGWAVE_TEST_DB = os.environ.get("IBIS_TEST_RISINGWAVE_DATABASE", "dev") IBIS_RISINGWAVE_HOST = os.environ.get("IBIS_TEST_RISINGWAVE_HOST", "localhost") diff --git a/ibis/backends/risingwave/tests/test_functions.py b/ibis/backends/risingwave/tests/test_functions.py index 38be06b1281d..dafbd315d98d 100644 --- a/ibis/backends/risingwave/tests/test_functions.py +++ b/ibis/backends/risingwave/tests/test_functions.py @@ -14,7 +14,7 @@ import ibis.expr.datatypes as dt from ibis import literal as L -pytest.importorskip("psycopg2") +pytest.importorskip("psycopg") @pytest.mark.parametrize(("value", "expected"), [(0, None), (5.5, 5.5)]) diff --git a/ibis/backends/tests/errors.py b/ibis/backends/tests/errors.py index 53e25e90f170..5824deec489f 100644 --- a/ibis/backends/tests/errors.py +++ b/ibis/backends/tests/errors.py @@ -131,25 +131,6 @@ PsycoPgOperationalError ) = PsycoPgUndefinedObject = PsycoPgArraySubscriptError = None -try: - from psycopg2.errors import ArraySubscriptError as PsycoPg2ArraySubscriptError - from psycopg2.errors import DivisionByZero as PsycoPg2DivisionByZero - from psycopg2.errors import IndeterminateDatatype as PsycoPg2IndeterminateDatatype - from psycopg2.errors import InternalError_ as PsycoPg2InternalError - from psycopg2.errors import ( - InvalidTextRepresentation as PsycoPg2InvalidTextRepresentation, - ) - from psycopg2.errors import OperationalError as PsycoPg2OperationalError - from psycopg2.errors import ProgrammingError as PsycoPg2ProgrammingError - from psycopg2.errors import SyntaxError as PsycoPg2SyntaxError - from psycopg2.errors import UndefinedObject as PsycoPg2UndefinedObject -except ImportError: - PsycoPg2SyntaxError = PsycoPg2IndeterminateDatatype = ( - PsycoPg2InvalidTextRepresentation - ) = PsycoPg2DivisionByZero = PsycoPg2InternalError = PsycoPg2ProgrammingError = ( - PsycoPg2OperationalError - ) = PsycoPg2UndefinedObject = PsycoPg2ArraySubscriptError = None - try: from MySQLdb import NotSupportedError as MySQLNotSupportedError from MySQLdb import OperationalError as MySQLOperationalError diff --git a/ibis/backends/tests/test_aggregation.py b/ibis/backends/tests/test_aggregation.py index 05babb55115c..b1d6e499c6a4 100644 --- a/ibis/backends/tests/test_aggregation.py +++ b/ibis/backends/tests/test_aggregation.py @@ -21,7 +21,7 @@ MySQLNotSupportedError, OracleDatabaseError, PolarsInvalidOperationError, - PsycoPg2InternalError, + PsycoPgInternalError, Py4JError, Py4JJavaError, PyAthenaOperationalError, @@ -963,7 +963,7 @@ def test_approx_quantile(con, filtered, multi): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function covar_pop(integer, integer) does not exist", ), ], @@ -983,7 +983,7 @@ def test_approx_quantile(con, filtered, multi): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function covar_pop(integer, integer) does not exist", ), ], @@ -1005,7 +1005,7 @@ def test_approx_quantile(con, filtered, multi): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function covar_pop(integer, integer) does not exist", ), ], @@ -1062,7 +1062,7 @@ def test_approx_quantile(con, filtered, multi): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function covar_pop(integer, integer) does not exist", ), ], @@ -1088,7 +1088,7 @@ def test_approx_quantile(con, filtered, multi): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function covar_pop(integer, integer) does not exist", ), ], diff --git a/ibis/backends/tests/test_array.py b/ibis/backends/tests/test_array.py index d47f2d7e085a..070f885d2219 100644 --- a/ibis/backends/tests/test_array.py +++ b/ibis/backends/tests/test_array.py @@ -22,12 +22,10 @@ GoogleBadRequest, MySQLOperationalError, PolarsComputeError, - PsycoPg2IndeterminateDatatype, - PsycoPg2InternalError, - PsycoPg2ProgrammingError, PsycoPgIndeterminateDatatype, PsycoPgInternalError, PsycoPgInvalidTextRepresentation, + PsycoPgProgrammingError, PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, @@ -506,7 +504,7 @@ def test_array_slice(backend, start, stop): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="TODO(Kexiang): seems a bug", ) @pytest.mark.notimpl(["athena"], raises=PyAthenaDatabaseError) @@ -565,7 +563,7 @@ def test_array_map(con, input, output, func): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="TODO(Kexiang): seems a bug", ) @pytest.mark.notimpl(["athena"], raises=PyAthenaDatabaseError) @@ -646,7 +644,7 @@ def test_array_map_with_index(con, input, output, func): ) @pytest.mark.notyet( "risingwave", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="no support for not null column constraint", ) @pytest.mark.parametrize( @@ -693,7 +691,7 @@ def test_array_filter(con, input, output, predicate): ) @pytest.mark.notyet( "risingwave", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="no support for not null column constraint", ) @pytest.mark.parametrize( @@ -740,7 +738,7 @@ def test_array_filter_with_index(con, input, output, predicate): ) @pytest.mark.notyet( "risingwave", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="no support for not null column constraint", ) @pytest.mark.parametrize( @@ -1097,7 +1095,7 @@ def test_array_intersect(con, data): @builtin_array @pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) -@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError) +@pytest.mark.notimpl(["risingwave"], raises=PsycoPgInternalError) @pytest.mark.notimpl( ["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError ) @@ -1117,7 +1115,7 @@ def test_unnest_struct(con): @builtin_array @pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) -@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError) +@pytest.mark.notimpl(["risingwave"], raises=PsycoPgInternalError) @pytest.mark.notimpl( ["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError ) @@ -1208,7 +1206,7 @@ def test_zip_null(con, fn): @builtin_array @pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) -@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError) +@pytest.mark.notimpl(["risingwave"], raises=PsycoPgProgrammingError) @pytest.mark.notimpl(["datafusion"], raises=Exception, reason="not yet supported") @pytest.mark.notimpl( ["polars"], @@ -1291,8 +1289,8 @@ def flatten_data(): reason="Risingwave doesn't truly support arrays of arrays", raises=( com.OperationNotDefinedError, - PsycoPg2IndeterminateDatatype, - PsycoPg2InternalError, + PsycoPgIndeterminateDatatype, + PsycoPgInternalError, ), ) @pytest.mark.parametrize( @@ -1399,7 +1397,7 @@ def test_range_start_stop_step(con, start, stop, step): @pytest.mark.notimpl(["flink"], raises=com.OperationNotDefinedError) @pytest.mark.never( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Invalid parameter step: step size cannot equal zero", ) def test_range_start_stop_step_zero(con, start, stop): @@ -1432,7 +1430,7 @@ def test_unnest_empty_array(con): @pytest.mark.notimpl(["sqlite"], raises=com.UnsupportedBackendType) @pytest.mark.notyet( "risingwave", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="no support for not null column constraint", ) @pytest.mark.notimpl(["athena"], raises=PyAthenaDatabaseError) @@ -1515,7 +1513,7 @@ def swap(token): id="pos", marks=pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function make_interval() does not exist", ), ), @@ -1533,7 +1531,7 @@ def swap(token): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function neg(interval) does not exist", ), ], @@ -1553,7 +1551,7 @@ def swap(token): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function neg(interval) does not exist", ), ], @@ -1585,7 +1583,7 @@ def test_timestamp_range(con, start, stop, step, freq, tzinfo): pytest.mark.notyet(["polars"], raises=PolarsComputeError), pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function make_interval() does not exist", ), ], @@ -1604,7 +1602,7 @@ def test_timestamp_range(con, start, stop, step, freq, tzinfo): ), pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function neg(interval) does not exist", ), ], @@ -1760,7 +1758,7 @@ def test_table_unnest_with_keep_empty(con): ["datafusion", "polars", "flink"], raises=com.OperationNotDefinedError ) @pytest.mark.notyet( - ["risingwave"], raises=PsycoPg2InternalError, reason="not supported in risingwave" + ["risingwave"], raises=PsycoPgInternalError, reason="not supported in risingwave" ) @pytest.mark.notimpl( ["athena"], @@ -1781,9 +1779,9 @@ def test_table_unnest_column_expr(backend): @pytest.mark.notimpl(["trino"], raises=TrinoUserError) @pytest.mark.notimpl(["athena"], raises=PyAthenaOperationalError) @pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError) -@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError) +@pytest.mark.notimpl(["risingwave"], raises=PsycoPgProgrammingError) @pytest.mark.notyet( - ["risingwave"], raises=PsycoPg2InternalError, reason="not supported in risingwave" + ["risingwave"], raises=PsycoPgInternalError, reason="not supported in risingwave" ) def test_table_unnest_array_of_struct_of_array(con): t = ibis.memtable( diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index 10c87bcd592a..ac89c02aaf9d 100644 --- a/ibis/backends/tests/test_client.py +++ b/ibis/backends/tests/test_client.py @@ -31,7 +31,7 @@ ExaQueryError, ImpalaHiveServer2Error, OracleDatabaseError, - PsycoPg2InternalError, + PsycoPgInternalError, PsycoPgUndefinedObject, Py4JJavaError, PyAthenaDatabaseError, @@ -415,7 +415,7 @@ def test_rename_table(con, temp_table, temp_table_orig): ) @pytest.mark.never( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason='Feature is not yet implemented: column constraints "NOT NULL"', ) def test_nullable_input_output(con, temp_table): @@ -538,7 +538,7 @@ def test_insert_no_overwrite_from_dataframe( @pytest.mark.notimpl(["polars"], reason="`insert` method not implemented") @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="truncate not supported upstream", ) @pytest.mark.notyet( @@ -584,7 +584,7 @@ def test_insert_no_overwrite_from_expr( ) @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="truncate not supported upstream", ) def test_insert_overwrite_from_expr( @@ -608,7 +608,7 @@ def test_insert_overwrite_from_expr( ) @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="truncate not supported upstream", ) def test_insert_overwrite_from_list(con, employee_data_1_temp_table): @@ -737,7 +737,7 @@ def test_list_database_contents(con): @pytest.mark.notyet(["impala"], raises=ImpalaHiveServer2Error) @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="unsigned integers are not supported", ) @pytest.mark.notimpl( diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py index 267a0bc0e5ed..7df01e99db66 100644 --- a/ibis/backends/tests/test_generic.py +++ b/ibis/backends/tests/test_generic.py @@ -24,7 +24,7 @@ MySQLProgrammingError, OracleDatabaseError, PolarsInvalidOperationError, - PsycoPg2InternalError, + PsycoPgInternalError, PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, @@ -1136,7 +1136,7 @@ def test_typeof(con): @pytest.mark.notyet(["exasol"], raises=ExaQueryError, reason="not supported by exasol") @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="https://github.com/risingwavelabs/risingwave/issues/1343", ) @pytest.mark.notyet( @@ -1740,7 +1740,7 @@ def hash_256(col): pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl(["oracle"], raises=OracleDatabaseError), pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError), - pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError), + pytest.mark.notimpl(["risingwave"], raises=PsycoPgInternalError), pytest.mark.notimpl(["snowflake"], raises=AssertionError), pytest.mark.never( ["datafusion", "exasol", "impala", "mssql", "mysql", "sqlite"], @@ -2077,7 +2077,7 @@ def test_static_table_slice(backend, slc, expected_count_fn): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="risingwave doesn't support limit/offset", ) @pytest.mark.notyet( @@ -2178,7 +2178,7 @@ def test_dynamic_table_slice(backend, slc, expected_count_fn): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="risingwave doesn't support limit/offset", ) def test_dynamic_table_slice_with_computed_offset(backend): diff --git a/ibis/backends/tests/test_map.py b/ibis/backends/tests/test_map.py index 6c595b56b6e3..8524ab649a8d 100644 --- a/ibis/backends/tests/test_map.py +++ b/ibis/backends/tests/test_map.py @@ -7,7 +7,7 @@ import ibis.common.exceptions as exc import ibis.expr.datatypes as dt from ibis.backends.tests.errors import ( - PsycoPg2InternalError, + PsycoPgInternalError, Py4JJavaError, PyAthenaOperationalError, ) @@ -51,7 +51,7 @@ @pytest.mark.notyet("clickhouse", reason="nested types can't be NULL") @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function hstore(character varying[], character varying[]) does not exist", ) @pytest.mark.parametrize( @@ -73,7 +73,7 @@ def test_map_nulls(con, k, v): @pytest.mark.notyet("clickhouse", reason="nested types can't be NULL") @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function hstore(character varying[], character varying[]) does not exist", ) @pytest.mark.parametrize( @@ -94,7 +94,7 @@ def test_map_keys_nulls(con, k, v): @pytest.mark.notyet("clickhouse", reason="nested types can't be NULL") @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function hstore(character varying[], character varying[]) does not exist", ) @pytest.mark.parametrize( @@ -123,7 +123,7 @@ def test_map_values_nulls(con, map): @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function hstore(character varying[], character varying[]) does not exist", ) @pytest.mark.parametrize( @@ -194,7 +194,7 @@ def test_map_get_contains_nulls(con, map, key, method): @pytest.mark.notyet("clickhouse", reason="nested types can't be NULL") @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function hstore(character varying[], character varying[]) does not exist", ) @pytest.mark.parametrize( diff --git a/ibis/backends/tests/test_numeric.py b/ibis/backends/tests/test_numeric.py index a8135b084122..7c5a944e0b4e 100644 --- a/ibis/backends/tests/test_numeric.py +++ b/ibis/backends/tests/test_numeric.py @@ -22,8 +22,8 @@ ImpalaHiveServer2Error, MySQLOperationalError, OracleDatabaseError, - PsycoPg2InternalError, PsycoPgDivisionByZero, + PsycoPgInternalError, Py4JError, Py4JJavaError, PyAthenaOperationalError, @@ -783,7 +783,7 @@ def test_isnan_isinf( pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function log10(numeric, numeric) does not exist", ), ], @@ -802,7 +802,7 @@ def test_isnan_isinf( pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function log10(numeric, numeric) does not exist", ), ], @@ -1001,7 +1001,7 @@ def test_floor_divide_precedence(con): pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function log10(numeric, numeric) does not exist", ), ], @@ -1014,7 +1014,7 @@ def test_floor_divide_precedence(con): pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function log10(numeric, numeric) does not exist", ), ], @@ -1053,7 +1053,7 @@ def test_floor_divide_precedence(con): pytest.mark.notimpl(["polars"], raises=com.UnsupportedArgumentError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function log10(numeric, numeric) does not exist", ), ], diff --git a/ibis/backends/tests/test_param.py b/ibis/backends/tests/test_param.py index 27282ce8168d..f64ac4c1b66d 100644 --- a/ibis/backends/tests/test_param.py +++ b/ibis/backends/tests/test_param.py @@ -9,7 +9,7 @@ import ibis import ibis.expr.datatypes as dt from ibis import _ -from ibis.backends.tests.errors import OracleDatabaseError, PsycoPg2InternalError +from ibis.backends.tests.errors import OracleDatabaseError, PsycoPgInternalError np = pytest.importorskip("numpy") pd = pytest.importorskip("pandas") @@ -101,7 +101,7 @@ def test_scalar_param_struct(con): @pytest.mark.notyet(["bigquery"]) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function make_date(integer, integer, integer) does not exist", ) def test_scalar_param_map(con): diff --git a/ibis/backends/tests/test_set_ops.py b/ibis/backends/tests/test_set_ops.py index c459a4055346..d1bcb534856e 100644 --- a/ibis/backends/tests/test_set_ops.py +++ b/ibis/backends/tests/test_set_ops.py @@ -8,7 +8,7 @@ import ibis import ibis.expr.types as ir from ibis import _ -from ibis.backends.tests.errors import PsycoPg2InternalError, PyDruidProgrammingError +from ibis.backends.tests.errors import PsycoPgInternalError, PyDruidProgrammingError pd = pytest.importorskip("pandas") @@ -74,7 +74,7 @@ def test_union_mixed_distinct(backend, union_subsets): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: INTERSECT all", ), ], @@ -118,7 +118,7 @@ def test_intersect(backend, alltypes, df, distinct): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: EXCEPT all", ), ], @@ -208,7 +208,7 @@ def test_top_level_union(backend, con, alltypes, distinct, ordered): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: INTERSECT all", ), ], diff --git a/ibis/backends/tests/test_string.py b/ibis/backends/tests/test_string.py index 7ee55636fd05..1d4a7c3e817b 100644 --- a/ibis/backends/tests/test_string.py +++ b/ibis/backends/tests/test_string.py @@ -14,7 +14,7 @@ ClickHouseDatabaseError, MySQLOperationalError, OracleDatabaseError, - PsycoPg2InternalError, + PsycoPgInternalError, PyODBCProgrammingError, ) from ibis.common.annotations import ValidationError @@ -70,7 +70,7 @@ ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason='sql parser error: Expected end of statement, found: "NG\'" at line:1, column:31 Near "SELECT \'STRI"NG\' AS "\'STRI""', ), ], @@ -100,7 +100,7 @@ ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason='sql parser error: Expected end of statement, found: "NG\'" at line:1, column:31 Near "SELECT \'STRI"NG\' AS "\'STRI""', ), ], @@ -879,7 +879,7 @@ def test_multiple_subs(con): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function levenshtein(character varying, character varying) does not exist", ) @pytest.mark.parametrize( diff --git a/ibis/backends/tests/test_struct.py b/ibis/backends/tests/test_struct.py index 48e046cb3a54..ece0b5530654 100644 --- a/ibis/backends/tests/test_struct.py +++ b/ibis/backends/tests/test_struct.py @@ -12,7 +12,7 @@ from ibis.backends.tests.errors import ( DatabricksServerOperationError, PolarsColumnNotFoundError, - PsycoPg2InternalError, + PsycoPgInternalError, PsycoPgSyntaxError, Py4JJavaError, PyAthenaDatabaseError, @@ -143,7 +143,7 @@ def test_collect_into_struct(alltypes): @pytest.mark.notimpl( ["risingwave"], reason="struct literals not implemented", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, ) @pytest.mark.notyet(["datafusion"], raises=Exception, reason="unsupported syntax") @pytest.mark.notimpl(["flink"], raises=Py4JJavaError, reason="not implemented in ibis") @@ -181,7 +181,7 @@ def test_field_access_after_case(con): pytest.mark.notyet( ["risingwave"], reason="non-nullable struct types not implemented", - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, ), pytest.mark.notimpl( ["pyspark"], @@ -247,7 +247,7 @@ def test_keyword_fields(con, nullable): ) @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="sqlglot doesn't implement structs for postgres correctly", ) @pytest.mark.notyet( diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py index 11a2a3dacfbd..270c72a8aca6 100644 --- a/ibis/backends/tests/test_temporal.py +++ b/ibis/backends/tests/test_temporal.py @@ -29,7 +29,7 @@ OracleDatabaseError, PolarsInvalidOperationError, PolarsPanicException, - PsycoPg2InternalError, + PsycoPgInternalError, Py4JJavaError, PyAthenaOperationalError, PyDruidProgrammingError, @@ -494,7 +494,7 @@ def test_date_truncate(backend, alltypes, df, unit): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Bind error: Invalid unit: week", ), sqlite_without_ymd_intervals, @@ -518,7 +518,7 @@ def test_date_truncate(backend, alltypes, df, unit): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Bind error: Invalid unit: millisecond", ), sqlite_without_hms_intervals, @@ -543,7 +543,7 @@ def test_date_truncate(backend, alltypes, df, unit): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Bind error: Invalid unit: microsecond", ), ], @@ -612,7 +612,7 @@ def convert_to_offset(offset, displacement_type=displacement_type): pytest.mark.notyet(["oracle"], raises=com.UnsupportedArgumentError), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Bind error: Invalid unit: week", ), pytest.mark.notimpl( @@ -1977,7 +1977,7 @@ def test_large_timestamp(con): pytest.mark.notimpl(["exasol"], raises=AssertionError), pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Parse error: timestamp without time zone Can't cast string to timestamp (expected format is YYYY-MM-DD HH:MM:SS[.D+{up to 6 digits}] or YYYY-MM-DD HH:MM or YYYY-MM-DD or ISO 8601 format)", ), pytest.mark.notyet(["athena"], raises=PyAthenaOperationalError), @@ -2148,7 +2148,7 @@ def test_delta(con, start, end, unit, expected): @pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function date_bin(interval, timestamp without time zone, timestamp without time zone) does not exist", ) def test_timestamp_bucket(backend, kws, pd_freq): @@ -2182,7 +2182,7 @@ def test_timestamp_bucket(backend, kws, pd_freq): @pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="function date_bin(interval, timestamp without time zone, timestamp without time zone) does not exist", ) def test_timestamp_bucket_offset(backend, offset_mins): diff --git a/ibis/backends/tests/test_window.py b/ibis/backends/tests/test_window.py index dff9331a1d26..c60753fb490d 100644 --- a/ibis/backends/tests/test_window.py +++ b/ibis/backends/tests/test_window.py @@ -14,7 +14,7 @@ GoogleBadRequest, ImpalaHiveServer2Error, MySQLOperationalError, - PsycoPg2InternalError, + PsycoPgInternalError, Py4JJavaError, PyDruidProgrammingError, PyODBCProgrammingError, @@ -127,7 +127,7 @@ def calc_zscore(s): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: percent_rank", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -143,7 +143,7 @@ def calc_zscore(s): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: cume_dist", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -167,7 +167,7 @@ def calc_zscore(s): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: ntile", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -199,7 +199,7 @@ def calc_zscore(s): ["impala", "mssql"], raises=com.OperationNotDefinedError ), pytest.mark.notimpl(["flink"], raises=com.OperationNotDefinedError), - pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError), + pytest.mark.notimpl(["risingwave"], raises=PsycoPgInternalError), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), ], ), @@ -342,7 +342,7 @@ def test_grouped_bounded_expanding_window( marks=[ pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -614,7 +614,7 @@ def test_grouped_unbounded_window(backend, alltypes, df, result_fn, expected_fn) @pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) @@ -643,7 +643,7 @@ def test_simple_ungrouped_unbound_following_window( @pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ) def test_simple_ungrouped_window_with_scalar_order_by(alltypes): @@ -671,7 +671,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -692,7 +692,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): marks=[ pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: ntile", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -781,7 +781,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): marks=[ pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -808,7 +808,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -822,7 +822,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): marks=[ pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -852,7 +852,7 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): ), pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ), pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError), @@ -1018,7 +1018,7 @@ def gb_fn(df): ) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: percent_rank", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) @@ -1070,7 +1070,7 @@ def agg(df): @pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) @@ -1140,7 +1140,7 @@ def test_first_last(backend): @pytest.mark.notyet(["flink"], raises=Py4JJavaError, reason="bug in Flink") @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="sql parser error: Expected literal int, found: INTERVAL at line:1, column:99", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) @@ -1190,7 +1190,7 @@ def test_range_expression_bounds(backend): ) @pytest.mark.notyet( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Unrecognized window function: percent_rank", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) @@ -1222,7 +1222,7 @@ def test_rank_followed_by_over_call_merge_frames(backend, alltypes, df): @pytest.mark.notimpl(["polars"], raises=com.OperationNotDefinedError) @pytest.mark.notimpl( ["risingwave"], - raises=PsycoPg2InternalError, + raises=PsycoPgInternalError, reason="Feature is not yet implemented: Window function with empty PARTITION BY is not supported yet", ) @pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError) diff --git a/pyproject.toml b/pyproject.toml index 91f1a5918ca5..eba25e324a99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,7 +196,7 @@ sqlite = [ "rich>=12.4.4,<14", ] risingwave = [ - "psycopg2>=2.8.4,<3", + "psycopg>=3.2.0,<4", "pyarrow>=10.0.1", "pyarrow-hotfix>=0.4,<1", "numpy>=1.23.2,<3", diff --git a/requirements-dev.txt b/requirements-dev.txt index 9d41bb06c2b3..06ca5953a7f7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -179,7 +179,6 @@ proto-plus==1.25.0 protobuf==5.29.2 psutil==6.1.1 psycopg==3.2.3 -psycopg2==2.9.10 psygnal==0.11.1 ptyprocess==0.7.0 ; os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') pure-eval==0.2.3 diff --git a/uv.lock b/uv.lock index ef384bef6942..ee4aa341ab53 100644 --- a/uv.lock +++ b/uv.lock @@ -2159,7 +2159,7 @@ pyspark = [ risingwave = [ { name = "numpy" }, { name = "pandas" }, - { name = "psycopg2" }, + { name = "psycopg" }, { name = "pyarrow" }, { name = "pyarrow-hotfix" }, { name = "rich" }, @@ -2315,7 +2315,7 @@ requires-dist = [ { name = "pins", extras = ["gcs"], marker = "extra == 'examples'", specifier = ">=0.8.3,<1" }, { name = "polars", marker = "extra == 'polars'", specifier = ">=1,<2" }, { name = "psycopg", marker = "extra == 'postgres'", specifier = ">=3.2.0,<4" }, - { name = "psycopg2", marker = "extra == 'risingwave'", specifier = ">=2.8.4,<3" }, + { name = "psycopg", marker = "extra == 'risingwave'", specifier = ">=3.2.0,<4" }, { name = "pyarrow", marker = "extra == 'athena'", specifier = ">=10.0.1" }, { name = "pyarrow", marker = "extra == 'bigquery'", specifier = ">=10.0.1" }, { name = "pyarrow", marker = "extra == 'clickhouse'", specifier = ">=10.0.1" }, @@ -4016,21 +4016,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/21/534b8f5bd9734b7a2fcd3a16b1ee82ef6cad81a4796e95ebf4e0c6a24119/psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", size = 197934 }, ] -[[package]] -name = "psycopg2" -version = "2.9.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/a9/146b6bdc0d33539a359f5e134ee6dda9173fb8121c5b96af33fa299e50c4/psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716", size = 1024527 }, - { url = "https://files.pythonhosted.org/packages/47/50/c509e56f725fd2572b59b69bd964edaf064deebf1c896b2452f6b46fdfb3/psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a", size = 1163735 }, - { url = "https://files.pythonhosted.org/packages/20/a2/c51ca3e667c34e7852157b665e3d49418e68182081060231d514dd823225/psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2", size = 1024538 }, - { url = "https://files.pythonhosted.org/packages/33/39/5a9a229bb5414abeb86e33b8fc8143ab0aecce5a7f698a53e31367d30caa/psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4", size = 1163736 }, - { url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 }, - { url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 }, - { url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 }, -] - [[package]] name = "psygnal" version = "0.11.1"