From 8344a838db7c672b732e22a74203e1de05b7fc3a 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] 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 | 37 +++++++++---------- ibis/backends/postgres/tests/conftest.py | 2 +- ibis/backends/postgres/tests/test_client.py | 8 ++-- .../backends/postgres/tests/test_functions.py | 2 +- 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, 96 insertions(+), 54 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..bbf54ca1e27a 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): @@ -152,7 +150,7 @@ def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame: try: df = pd.DataFrame.from_records( - cursor, columns=schema.names, coerce_float=True + cursor.stream(), columns=schema.names, coerce_float=True ) except Exception: # clean up the cursor if we fail to create the DataFrame @@ -233,17 +231,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 +250,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 +699,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 +710,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..0e2a6e78100f 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( 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..2de2501bc942 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, + PsycoPgArraySubscriptError, + PsycoPgIndeterminateDatatype, + PsycoPgInternalError, + 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=PsycoPgArraySubscriptError, 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]]