diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 028e3533..218b15b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist -i 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 pypy3.11 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11 sccache: 'true' manylinux: auto before-script-linux: | @@ -71,7 +71,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist -i 3.9 3.10 3.11 3.12 3.13 3.14 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 @@ -112,7 +112,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist -i 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 pypy3.11 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11 sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 @@ -168,7 +168,7 @@ jobs: uses: messense/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist -i 3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 pypy3.11 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11 manylinux: musllinux_1_2 - name: Upload wheels uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 56deb6d4..193bc4d1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,7 +35,7 @@ jobs: name: ${{matrix.job.os}}-${{matrix.py_version}}-${{ matrix.postgres_version }} strategy: matrix: - py_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + py_version: ["3.10", "3.11", "3.12", "3.13", "3.14"] postgres_version: ["14", "15", "16", "17"] job: - os: ubuntu-latest diff --git a/examples/aiohttp/start_example.py b/examples/aiohttp/start_example.py index 1ac2e1f0..b6dfc047 100644 --- a/examples/aiohttp/start_example.py +++ b/examples/aiohttp/start_example.py @@ -18,12 +18,12 @@ async def start_db_pool(app: web.Application) -> None: async def stop_db_pool(app: web.Application) -> None: """Close database connection pool.""" - db_pool = cast(PSQLPool, app.db_pool) + db_pool = cast("PSQLPool", app.db_pool) await db_pool.close() async def pg_pool_example(request: web.Request) -> Any: - db_pool = cast(PSQLPool, request.app["db_pool"]) + db_pool = cast("PSQLPool", request.app["db_pool"]) connection = await db_pool.connection() await asyncio.sleep(10) query_result = await connection.execute( diff --git a/examples/fastapi/advanced_example.py b/examples/fastapi/advanced_example.py index 934e4ed2..1cbc7074 100644 --- a/examples/fastapi/advanced_example.py +++ b/examples/fastapi/advanced_example.py @@ -1,7 +1,7 @@ # Start example import asyncio +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator import uvicorn from fastapi import FastAPI diff --git a/examples/fastapi/start_example.py b/examples/fastapi/start_example.py index e0277926..8e415c89 100644 --- a/examples/fastapi/start_example.py +++ b/examples/fastapi/start_example.py @@ -1,12 +1,12 @@ # Start example +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator, cast +from typing import Annotated, cast import uvicorn from fastapi import Depends, FastAPI, Request from fastapi.responses import JSONResponse from psqlpy import Connection, PSQLPool -from typing_extensions import Annotated @asynccontextmanager @@ -26,7 +26,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: async def db_connection(request: Request) -> Connection: """Retrieve new connection from connection pool and return it.""" - return await cast(PSQLPool, request.app.state.db_pool).connection() + return await cast("PSQLPool", request.app.state.db_pool).connection() @app.get("/") diff --git a/pyproject.toml b/pyproject.toml index 396b5086..0077e54a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "psqlpy" -requires-python = ">=3.8" +requires-python = ">=3.10" keywords = [ "postgresql", "psql", @@ -27,8 +27,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index ee41556a..336bc311 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -1,11 +1,12 @@ import types import typing +from collections.abc import Awaitable, Callable, Mapping, Sequence from enum import Enum from io import BytesIO from ipaddress import IPv4Address, IPv6Address -from typing import Any, Awaitable, Callable, Mapping, Sequence, TypeVar +from typing import Any, TypeAlias, TypeVar -from typing_extensions import Buffer, Self, TypeAlias +from typing_extensions import Buffer, Self _CustomClass = TypeVar( "_CustomClass", @@ -22,7 +23,7 @@ class QueryResult: @typing.overload def result( self: Self, - as_tuple: typing.Literal[None] = None, + as_tuple: None = None, custom_decoders: dict[str, Callable[[bytes], Any]] | None = None, ) -> list[dict[str, Any]]: ... @typing.overload @@ -112,7 +113,7 @@ class SingleQueryResult: @typing.overload def result( self: Self, - as_tuple: typing.Literal[None] = None, + as_tuple: None = None, custom_decoders: dict[str, Callable[[bytes], Any]] | None = None, ) -> dict[str, Any]: ... @typing.overload diff --git a/python/psqlpy/_internal/extra_types.pyi b/python/psqlpy/_internal/extra_types.pyi index e29c7573..2b0dc6d2 100644 --- a/python/psqlpy/_internal/extra_types.pyi +++ b/python/psqlpy/_internal/extra_types.pyi @@ -2,9 +2,10 @@ import typing from datetime import date, datetime, time, timedelta from decimal import Decimal from ipaddress import IPv4Address, IPv6Address +from typing import TypeAlias from uuid import UUID -from typing_extensions import Self, TypeAlias +from typing_extensions import Self class SmallInt: """Represent SmallInt in PostgreSQL and `i16` in Rust.""" diff --git a/python/tests/conftest.py b/python/tests/conftest.py index a6ea6e8e..e0c0a699 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,6 +1,6 @@ import os import random -from typing import AsyncGenerator +from collections.abc import AsyncGenerator from urllib import parse import pytest diff --git a/python/tests/test_kwargs_parameters.py b/python/tests/test_kwargs_parameters.py index 885e99f9..d5dc2f68 100644 --- a/python/tests/test_kwargs_parameters.py +++ b/python/tests/test_kwargs_parameters.py @@ -77,6 +77,6 @@ async def test_failed_no_parameter( async with psql_pool.acquire() as conn: with pytest.raises(expected_exception=PyToRustValueMappingError): await conn.execute( - querystring=(f"SELECT * FROM {table_name} " "WHERE name = $(name)p"), # noqa: ISC001 + querystring=(f"SELECT * FROM {table_name} WHERE name = $(name)p"), parameters={"mistake": "wow"}, ) diff --git a/python/tests/test_row_factories.py b/python/tests/test_row_factories.py index 9b7f3121..481e81c3 100644 --- a/python/tests/test_row_factories.py +++ b/python/tests/test_row_factories.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Dict, Type +from typing import Any import pytest from psqlpy import ConnectionPool @@ -53,9 +54,9 @@ class ValidationTestModel: name: str def to_class( - class_: Type[ValidationTestModel], - ) -> Callable[[Dict[str, Any]], ValidationTestModel]: - def to_class_inner(row: Dict[str, Any]) -> ValidationTestModel: + class_: type[ValidationTestModel], + ) -> Callable[[dict[str, Any]], ValidationTestModel]: + def to_class_inner(row: dict[str, Any]) -> ValidationTestModel: return class_(**row) return to_class_inner diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py index 343bb868..81dc7e2c 100644 --- a/python/tests/test_transaction.py +++ b/python/tests/test_transaction.py @@ -37,11 +37,14 @@ async def test_transaction_init_parameters( deferrable: bool | None, read_variant: ReadVariant | None, ) -> None: - async with psql_pool.acquire() as connection, connection.transaction( - isolation_level=isolation_level, - deferrable=deferrable, - read_variant=read_variant, - ) as transaction: + async with ( + psql_pool.acquire() as connection, + connection.transaction( + isolation_level=isolation_level, + deferrable=deferrable, + read_variant=read_variant, + ) as transaction, + ): await transaction.execute("SELECT 1") try: await transaction.execute( diff --git a/python/tests/test_value_converter.py b/python/tests/test_value_converter.py index 015ffed0..1ede7447 100644 --- a/python/tests/test_value_converter.py +++ b/python/tests/test_value_converter.py @@ -1,10 +1,10 @@ import datetime -import sys import uuid +import zoneinfo from decimal import Decimal from enum import Enum from ipaddress import IPv4Address -from typing import Any, Dict, List, Tuple, Union +from typing import Annotated, Any import pytest from psqlpy import ConnectionPool @@ -54,7 +54,6 @@ VarCharArray, ) from pydantic import BaseModel -from typing_extensions import Annotated from tests.conftest import DefaultPydanticModel, DefaultPythonModelClass @@ -82,19 +81,17 @@ 142574, tzinfo=datetime.timezone.utc, ) -if sys.version_info >= (3, 9): - import zoneinfo - - now_datetime_with_tz_in_asia_jakarta = datetime.datetime( - 2024, - 4, - 13, - 17, - 3, - 46, - 142574, - tzinfo=zoneinfo.ZoneInfo(key="Asia/Jakarta"), - ) + +now_datetime_with_tz_in_asia_jakarta = datetime.datetime( + 2024, + 4, + 13, + 17, + 3, + 46, + 142574, + tzinfo=zoneinfo.ZoneInfo(key="Asia/Jakarta"), +) async def test_as_class( @@ -147,6 +144,9 @@ async def test_as_class( ("MONEY", Money(99999999999999999), 99999999999999999), ("MONEY", 99999999999999999, 99999999999999999), ("NUMERIC(5, 2)", Decimal("120.12"), Decimal("120.12")), + ("NUMERIC(5, 2)", Decimal("120.123"), Decimal("120.12")), + ("NUMERIC(5, 2)", Decimal(120), Decimal(120)), + ("NUMERIC(5, 3)", Decimal("12.123"), Decimal("12.123")), ("FLOAT4", Float32(32.12329864501953), 32.12329864501953), ("FLOAT4", 32.12329864501953, 32.12329864501953), ("FLOAT8", Float64(32.12329864501953), 32.12329864501953), @@ -520,37 +520,37 @@ class ValidateModelForCustomType(BaseModel): timestampz_: datetime.datetime uuid_: uuid.UUID inet_: IPv4Address - jsonb_: Dict[str, List[Union[str, int, List[str]]]] - json_: Dict[str, List[Union[str, int, List[str]]]] - point_: Tuple[float, float] - box_: Tuple[Tuple[float, float], Tuple[float, float]] - path_: List[Tuple[float, float]] - line_: Annotated[List[float], 3] - lseg_: Annotated[List[Tuple[float, float]], 2] - circle_: Tuple[Tuple[float, float], float] - - varchar_arr: List[str] - varchar_arr_mdim: List[List[str]] - text_arr: List[str] - bool_arr: List[bool] - int2_arr: List[int] - int4_arr: List[int] - int8_arr: List[int] - float8_arr: List[float] - date_arr: List[datetime.date] - time_arr: List[datetime.time] - timestamp_arr: List[datetime.datetime] - timestampz_arr: List[datetime.datetime] - uuid_arr: List[uuid.UUID] - inet_arr: List[IPv4Address] - jsonb_arr: List[Dict[str, List[Union[str, int, List[str]]]]] - json_arr: List[Dict[str, List[Union[str, int, List[str]]]]] - point_arr: List[Tuple[float, float]] - box_arr: List[Tuple[Tuple[float, float], Tuple[float, float]]] - path_arr: List[List[Tuple[float, float]]] - line_arr: List[Annotated[List[float], 3]] - lseg_arr: List[Annotated[List[Tuple[float, float]], 2]] - circle_arr: List[Tuple[Tuple[float, float], float]] + jsonb_: dict[str, list[str | int | list[str]]] + json_: dict[str, list[str | int | list[str]]] + point_: tuple[float, float] + box_: tuple[tuple[float, float], tuple[float, float]] + path_: list[tuple[float, float]] + line_: Annotated[list[float], 3] + lseg_: Annotated[list[tuple[float, float]], 2] + circle_: tuple[tuple[float, float], float] + + varchar_arr: list[str] + varchar_arr_mdim: list[list[str]] + text_arr: list[str] + bool_arr: list[bool] + int2_arr: list[int] + int4_arr: list[int] + int8_arr: list[int] + float8_arr: list[float] + date_arr: list[datetime.date] + time_arr: list[datetime.time] + timestamp_arr: list[datetime.datetime] + timestampz_arr: list[datetime.datetime] + uuid_arr: list[uuid.UUID] + inet_arr: list[IPv4Address] + jsonb_arr: list[dict[str, list[str | int | list[str]]]] + json_arr: list[dict[str, list[str | int | list[str]]]] + point_arr: list[tuple[float, float]] + box_arr: list[tuple[tuple[float, float], tuple[float, float]]] + path_arr: list[list[tuple[float, float]]] + line_arr: list[Annotated[list[float], 3]] + lseg_arr: list[Annotated[list[tuple[float, float]], 2]] + circle_arr: list[tuple[tuple[float, float], float]] test_inner_value: ValidateModelForInnerValueType test_enum_type: TestEnum @@ -695,7 +695,7 @@ async def test_row_factory_query_result( f"SELECT * FROM {table_name}", ) - def row_factory(db_result: Dict[str, Any]) -> List[str]: + def row_factory(db_result: dict[str, Any]) -> list[str]: return list(db_result.keys()) as_row_factory = select_result.row_factory( @@ -715,7 +715,7 @@ async def test_row_factory_single_query_result( f"SELECT * FROM {table_name} LIMIT 1", ) - def row_factory(db_result: Dict[str, Any]) -> List[str]: + def row_factory(db_result: dict[str, Any]) -> list[str]: return list(db_result.keys()) as_row_factory = select_result.row_factory( @@ -830,11 +830,37 @@ async def test_empty_array( NumericArray([Decimal("121.23"), Decimal("188.99")]), [Decimal("121.23"), Decimal("188.99")], ), + ( + "NUMERIC(5, 2) ARRAY", + NumericArray([Decimal("121.123"), Decimal("188.99")]), + [ + Decimal("121.12").quantize(Decimal("100.00")), + Decimal("188.99").quantize(Decimal("100.00")), + ], + ), + ( + "NUMERIC(5, 2) ARRAY", + NumericArray([Decimal(121), Decimal(188)]), + [Decimal(121), Decimal(188)], + ), ( "NUMERIC(5, 2) ARRAY", NumericArray([[Decimal("121.23")], [Decimal("188.99")]]), [[Decimal("121.23")], [Decimal("188.99")]], ), + ( + "NUMERIC(5, 2) ARRAY", + NumericArray([[Decimal("121.123")], [Decimal("188.99")]]), + [ + [Decimal("121.12").quantize(Decimal("100.00"))], + [Decimal("188.99").quantize(Decimal("100.00"))], + ], + ), + ( + "NUMERIC(5, 2) ARRAY", + NumericArray([[Decimal(121)], [Decimal(188)]]), + [[Decimal(121)], [Decimal(188)]], + ), ("FLOAT4 ARRAY", [], []), ( "FLOAT4 ARRAY", diff --git a/src/value_converter/dto/converter_impls.rs b/src/value_converter/dto/converter_impls.rs index 35f7bbde..14e436de 100644 --- a/src/value_converter/dto/converter_impls.rs +++ b/src/value_converter/dto/converter_impls.rs @@ -1,4 +1,4 @@ -use std::net::IpAddr; +use std::{net::IpAddr, str::FromStr}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; use pg_interval::Interval; @@ -126,9 +126,12 @@ construct_extra_type_converter!(extra_types::CustomType, PythonDTO::PyCustomType impl ToPythonDTO for PythonDecimal { fn to_python_dto(python_param: &pyo3::Bound<'_, PyAny>) -> PSQLPyResult { - Ok(PythonDTO::PyDecimal(Decimal::from_str_exact( - python_param.str()?.extract::<&str>()?, - )?)) + let string = python_param.str()?; + let string_extract = string.extract::<&str>()?; + if let Ok(possible_r_decimal) = Decimal::from_str_exact(string_extract) { + return Ok(PythonDTO::PyDecimal(possible_r_decimal)); + } + Ok(PythonDTO::PyDecimal(Decimal::from_str(string_extract)?)) } } diff --git a/tox.ini b/tox.ini index 91779db2..014909df 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,6 @@ env_list = py312 py311 py310 - py39 - py38 [gh] python = @@ -16,8 +14,6 @@ python = 3.12 = py312 3.11 = py311 3.10 = py310 - 3.9 = py39 - 3.8 = py38 [testenv] skip_install = true