diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b870c5e6..5fdd8830 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.2" + ".": "1.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f27760b7..662807a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # CHANGELOG +## [1.1.0](https://github.com/supabase/postgrest-py/compare/v1.0.2...v1.1.0) (2025-06-19) + + +### Features + +* allow injection of httpx client ([#591](https://github.com/supabase/postgrest-py/issues/591)) ([635a4ba](https://github.com/supabase/postgrest-py/commit/635a4ba421457ce0967c3efc332ae883b693ef71)) + + +### Bug Fixes + +* **pydantic:** model_validate_json causing code break with pydantic v1 ([#609](https://github.com/supabase/postgrest-py/issues/609)) ([587dcc8](https://github.com/supabase/postgrest-py/commit/587dcc82835afd0290c0c83f3c38ff6b8de123a2)) +* remove reliance on SyncClient and use Client directly from httpx ([#607](https://github.com/supabase/postgrest-py/issues/607)) ([021f1b6](https://github.com/supabase/postgrest-py/commit/021f1b65fd728116c715b33504df7c37847e6bf2)) + ## [1.0.2](https://github.com/supabase/postgrest-py/compare/v1.0.1...v1.0.2) (2025-05-21) diff --git a/Makefile b/Makefile index fc3c5ee8..c3bf20c6 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,11 @@ build_sync: run_unasync remove_pytest_asyncio_from_sync remove_pytest_asyncio_from_sync: sed -i 's/@pytest.mark.asyncio//g' tests/_sync/test_client.py sed -i 's/_async/_sync/g' tests/_sync/test_client.py - sed -i 's/Async/Sync/g' tests/_sync/test_client.py + sed -i 's/Async/Sync/g' postgrest/_sync/request_builder.py tests/_sync/test_client.py sed -i 's/_client\.SyncClient/_client\.Client/g' tests/_sync/test_client.py + sed -i 's/SyncHTTPTransport/HTTPTransport/g' tests/_sync/**.py + sed -i 's/SyncClient/Client/g' postgrest/_sync/**.py tests/_sync/**.py + sed -i 's/self\.session\.aclose/self\.session\.close/g' postgrest/_sync/client.py sleep: sleep 2 diff --git a/poetry.lock b/poetry.lock index 361f1493..a85e1c47 100644 --- a/poetry.lock +++ b/poetry.lock @@ -914,14 +914,14 @@ files = [ [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, - {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] @@ -1064,7 +1064,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["docs"] +groups = ["dev", "docs"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1075,37 +1075,38 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.26.0" +version = "1.0.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, - {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, + {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, + {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, ] [package.dependencies] @@ -1118,19 +1119,20 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, - {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" +pluggy = ">=1.2" +pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] @@ -1218,19 +1220,19 @@ files = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -1607,14 +1609,14 @@ resolved_reference = "6a082ee36d5e8941622b70f6cbcaf8e7a5be339d" [[package]] name = "urllib3" -version = "2.3.0" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["docs"] +groups = ["dev", "docs"] files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] @@ -1668,4 +1670,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "f2bd73258c2835b6d7b9d737fbda33a92bed2e442714363225161e02977276d9" +content-hash = "a43f853a5768628ac93e63b3e5d8daa14ff0f151f7c0ec31aaa17bb2cc4509d9" diff --git a/postgrest/__init__.py b/postgrest/__init__.py index f060e684..edb87f2e 100644 --- a/postgrest/__init__.py +++ b/postgrest/__init__.py @@ -24,7 +24,39 @@ ) from .base_request_builder import APIResponse from .constants import DEFAULT_POSTGREST_CLIENT_HEADERS -from .deprecated_client import Client, PostgrestClient -from .deprecated_get_request_builder import GetRequestBuilder from .exceptions import APIError +from .types import ( + CountMethod, + Filters, + RequestMethod, + ReturnMethod, +) from .version import __version__ + +__all__ = [ + "AsyncPostgrestClient", + "AsyncFilterRequestBuilder", + "AsyncQueryRequestBuilder", + "AsyncRequestBuilder", + "AsyncRPCFilterRequestBuilder", + "AsyncSelectRequestBuilder", + "AsyncSingleRequestBuilder", + "AsyncMaybeSingleRequestBuilder", + "SyncPostgrestClient", + "SyncFilterRequestBuilder", + "SyncMaybeSingleRequestBuilder", + "SyncQueryRequestBuilder", + "SyncRequestBuilder", + "SyncRPCFilterRequestBuilder", + "SyncSelectRequestBuilder", + "SyncSingleRequestBuilder", + "APIResponse", + "DEFAULT_POSTGREST_CLIENT_HEADERS", + "APIError", + "CountMethod", + "Filters", + "RequestMethod", + "ReturnMethod", + "Timeout", + "__version__", +] diff --git a/postgrest/_async/client.py b/postgrest/_async/client.py index b2994d32..63fb95e5 100644 --- a/postgrest/_async/client.py +++ b/postgrest/_async/client.py @@ -1,9 +1,10 @@ from __future__ import annotations from typing import Any, Dict, Optional, Union, cast +from warnings import warn from deprecation import deprecated -from httpx import Headers, QueryParams, Timeout +from httpx import AsyncClient, Headers, QueryParams, Timeout from ..base_client import BasePostgrestClient from ..constants import ( @@ -11,7 +12,6 @@ DEFAULT_POSTGREST_CLIENT_TIMEOUT, ) from ..types import CountMethod -from ..utils import AsyncClient from ..version import __version__ from .request_builder import AsyncRequestBuilder, AsyncRPCFilterRequestBuilder @@ -27,18 +27,50 @@ def __init__( *, schema: str = "public", headers: Dict[str, str] = DEFAULT_POSTGREST_CLIENT_HEADERS, - timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - verify: bool = True, + timeout: Union[int, float, Timeout, None] = None, + verify: Optional[bool] = None, proxy: Optional[str] = None, + http_client: Optional[AsyncClient] = None, ) -> None: + if timeout is not None: + warn( + "The 'timeout' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + if verify is not None: + warn( + "The 'verify' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + if proxy is not None: + warn( + "The 'proxy' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.verify = bool(verify) if verify is not None else True + self.timeout = ( + timeout + if isinstance(timeout, Timeout) + else ( + int(abs(timeout)) + if timeout is not None + else DEFAULT_POSTGREST_CLIENT_TIMEOUT + ) + ) + BasePostgrestClient.__init__( self, base_url, schema=schema, headers=headers, - timeout=timeout, - verify=verify, + timeout=self.timeout, + verify=self.verify, proxy=proxy, + http_client=http_client, ) self.session = cast(AsyncClient, self.session) @@ -50,6 +82,15 @@ def create_session( verify: bool = True, proxy: Optional[str] = None, ) -> AsyncClient: + http_client = None + if isinstance(self.http_client, AsyncClient): + http_client = self.http_client + + if http_client is not None: + http_client.base_url = base_url + http_client.headers.update({**headers}) + return http_client + return AsyncClient( base_url=base_url, headers=headers, diff --git a/postgrest/_async/request_builder.py b/postgrest/_async/request_builder.py index fa5b856f..0625ef35 100644 --- a/postgrest/_async/request_builder.py +++ b/postgrest/_async/request_builder.py @@ -2,7 +2,7 @@ from typing import Any, Generic, Optional, TypeVar, Union -from httpx import Headers, QueryParams +from httpx import AsyncClient, Headers, QueryParams from pydantic import ValidationError from ..base_request_builder import ( @@ -20,7 +20,7 @@ ) from ..exceptions import APIError, APIErrorFromJSON, generate_default_error_message from ..types import ReturnMethod -from ..utils import AsyncClient, get_origin_and_cast +from ..utils import get_origin_and_cast, model_validate_json _ReturnT = TypeVar("_ReturnT") @@ -74,7 +74,7 @@ async def execute(self) -> APIResponse[_ReturnT]: return body return APIResponse[_ReturnT].from_http_request_response(r) else: - json_obj = APIErrorFromJSON.model_validate_json(r.content) + json_obj = model_validate_json(APIErrorFromJSON, r.content) raise APIError(dict(json_obj)) except ValidationError as e: raise APIError(generate_default_error_message(r)) @@ -122,7 +122,7 @@ async def execute(self) -> SingleAPIResponse[_ReturnT]: ): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok) return SingleAPIResponse[_ReturnT].from_http_request_response(r) else: - json_obj = APIErrorFromJSON.model_validate_json(r.content) + json_obj = model_validate_json(APIErrorFromJSON, r.content) raise APIError(dict(json_obj)) except ValidationError as e: raise APIError(generate_default_error_message(r)) diff --git a/postgrest/_sync/client.py b/postgrest/_sync/client.py index 1a27cfb2..908db515 100644 --- a/postgrest/_sync/client.py +++ b/postgrest/_sync/client.py @@ -1,9 +1,10 @@ from __future__ import annotations from typing import Any, Dict, Optional, Union, cast +from warnings import warn from deprecation import deprecated -from httpx import Headers, QueryParams, Timeout +from httpx import Client, Headers, QueryParams, Timeout from ..base_client import BasePostgrestClient from ..constants import ( @@ -11,7 +12,6 @@ DEFAULT_POSTGREST_CLIENT_TIMEOUT, ) from ..types import CountMethod -from ..utils import SyncClient from ..version import __version__ from .request_builder import SyncRequestBuilder, SyncRPCFilterRequestBuilder @@ -27,20 +27,52 @@ def __init__( *, schema: str = "public", headers: Dict[str, str] = DEFAULT_POSTGREST_CLIENT_HEADERS, - timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - verify: bool = True, + timeout: Union[int, float, Timeout, None] = None, + verify: Optional[bool] = None, proxy: Optional[str] = None, + http_client: Optional[Client] = None, ) -> None: + if timeout is not None: + warn( + "The 'timeout' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + if verify is not None: + warn( + "The 'verify' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + if proxy is not None: + warn( + "The 'proxy' parameter is deprecated. Please configure it in the http client instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.verify = bool(verify) if verify is not None else True + self.timeout = ( + timeout + if isinstance(timeout, Timeout) + else ( + int(abs(timeout)) + if timeout is not None + else DEFAULT_POSTGREST_CLIENT_TIMEOUT + ) + ) + BasePostgrestClient.__init__( self, base_url, schema=schema, headers=headers, - timeout=timeout, - verify=verify, + timeout=self.timeout, + verify=self.verify, proxy=proxy, + http_client=http_client, ) - self.session = cast(SyncClient, self.session) + self.session = cast(Client, self.session) def create_session( self, @@ -49,8 +81,17 @@ def create_session( timeout: Union[int, float, Timeout], verify: bool = True, proxy: Optional[str] = None, - ) -> SyncClient: - return SyncClient( + ) -> Client: + http_client = None + if isinstance(self.http_client, Client): + http_client = self.http_client + + if http_client is not None: + http_client.base_url = base_url + http_client.headers.update({**headers}) + return http_client + + return Client( base_url=base_url, headers=headers, timeout=timeout, @@ -79,7 +120,7 @@ def __exit__(self, exc_type, exc, tb) -> None: def aclose(self) -> None: """Close the underlying HTTP connections.""" - self.session.aclose() + self.session.close() def from_(self, table: str) -> SyncRequestBuilder[_TableT]: """Perform a table operation. diff --git a/postgrest/_sync/request_builder.py b/postgrest/_sync/request_builder.py index 4f8af88e..1a4538fb 100644 --- a/postgrest/_sync/request_builder.py +++ b/postgrest/_sync/request_builder.py @@ -2,7 +2,7 @@ from typing import Any, Generic, Optional, TypeVar, Union -from httpx import Headers, QueryParams +from httpx import Client, Headers, QueryParams from pydantic import ValidationError from ..base_request_builder import ( @@ -20,7 +20,7 @@ ) from ..exceptions import APIError, APIErrorFromJSON, generate_default_error_message from ..types import ReturnMethod -from ..utils import SyncClient, get_origin_and_cast +from ..utils import get_origin_and_cast, model_validate_json _ReturnT = TypeVar("_ReturnT") @@ -28,7 +28,7 @@ class SyncQueryRequestBuilder(Generic[_ReturnT]): def __init__( self, - session: SyncClient, + session: Client, path: str, http_method: str, headers: Headers, @@ -74,7 +74,7 @@ def execute(self) -> APIResponse[_ReturnT]: return body return APIResponse[_ReturnT].from_http_request_response(r) else: - json_obj = APIErrorFromJSON.model_validate_json(r.content) + json_obj = model_validate_json(APIErrorFromJSON, r.content) raise APIError(dict(json_obj)) except ValidationError as e: raise APIError(generate_default_error_message(r)) @@ -83,7 +83,7 @@ def execute(self) -> APIResponse[_ReturnT]: class SyncSingleRequestBuilder(Generic[_ReturnT]): def __init__( self, - session: SyncClient, + session: Client, path: str, http_method: str, headers: Headers, @@ -122,7 +122,7 @@ def execute(self) -> SingleAPIResponse[_ReturnT]: ): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok) return SingleAPIResponse[_ReturnT].from_http_request_response(r) else: - json_obj = APIErrorFromJSON.model_validate_json(r.content) + json_obj = model_validate_json(APIErrorFromJSON, r.content) raise APIError(dict(json_obj)) except ValidationError as e: raise APIError(generate_default_error_message(r)) @@ -152,7 +152,7 @@ def execute(self) -> Optional[SingleAPIResponse[_ReturnT]]: class SyncFilterRequestBuilder(BaseFilterRequestBuilder[_ReturnT], SyncQueryRequestBuilder[_ReturnT]): # type: ignore def __init__( self, - session: SyncClient, + session: Client, path: str, http_method: str, headers: Headers, @@ -173,7 +173,7 @@ class SyncRPCFilterRequestBuilder( ): def __init__( self, - session: SyncClient, + session: Client, path: str, http_method: str, headers: Headers, @@ -192,7 +192,7 @@ def __init__( class SyncSelectRequestBuilder(BaseSelectRequestBuilder[_ReturnT], SyncQueryRequestBuilder[_ReturnT]): # type: ignore def __init__( self, - session: SyncClient, + session: Client, path: str, http_method: str, headers: Headers, @@ -271,7 +271,7 @@ def csv(self) -> SyncSingleRequestBuilder[str]: class SyncRequestBuilder(Generic[_ReturnT]): - def __init__(self, session: SyncClient, path: str) -> None: + def __init__(self, session: Client, path: str) -> None: self.session = session self.path = path @@ -287,7 +287,7 @@ def select( *columns: The names of the columns to fetch. count: The method to use to get the count of rows returned. Returns: - :class:`AsyncSelectRequestBuilder` + :class:`SyncSelectRequestBuilder` """ method, params, headers, json = pre_select(*columns, count=count, head=head) return SyncSelectRequestBuilder[_ReturnT]( @@ -314,7 +314,7 @@ def insert( Otherwise, use the default value for the column. Only applies for bulk inserts. Returns: - :class:`AsyncQueryRequestBuilder` + :class:`SyncQueryRequestBuilder` """ method, params, headers, json = pre_insert( json, @@ -350,7 +350,7 @@ def upsert( not when merging with existing rows under `ignoreDuplicates: false`. This also only applies when doing bulk upserts. Returns: - :class:`AsyncQueryRequestBuilder` + :class:`SyncQueryRequestBuilder` """ method, params, headers, json = pre_upsert( json, @@ -378,7 +378,7 @@ def update( count: The method to use to get the count of rows returned. returning: Either 'minimal' or 'representation' Returns: - :class:`AsyncFilterRequestBuilder` + :class:`SyncFilterRequestBuilder` """ method, params, headers, json = pre_update( json, @@ -401,7 +401,7 @@ def delete( count: The method to use to get the count of rows returned. returning: Either 'minimal' or 'representation' Returns: - :class:`AsyncFilterRequestBuilder` + :class:`SyncFilterRequestBuilder` """ method, params, headers, json = pre_delete( count=count, diff --git a/postgrest/base_client.py b/postgrest/base_client.py index 2c9756ab..6fc596a6 100644 --- a/postgrest/base_client.py +++ b/postgrest/base_client.py @@ -3,9 +3,9 @@ from abc import ABC, abstractmethod from typing import Dict, Optional, Union -from httpx import BasicAuth, Timeout +from httpx import AsyncClient, BasicAuth, Client, Timeout -from .utils import AsyncClient, SyncClient, is_http_url, is_valid_jwt +from .utils import is_http_url, is_valid_jwt class BasePostgrestClient(ABC): @@ -20,6 +20,7 @@ def __init__( timeout: Union[int, float, Timeout], verify: bool = True, proxy: Optional[str] = None, + http_client: Union[Client, AsyncClient, None] = None, ) -> None: if not is_http_url(base_url): ValueError("base_url must be a valid HTTP URL string") @@ -33,8 +34,13 @@ def __init__( self.timeout = timeout self.verify = verify self.proxy = proxy + self.http_client = http_client self.session = self.create_session( - self.base_url, self.headers, self.timeout, self.verify, self.proxy + self.base_url, + self.headers, + self.timeout, + self.verify, + self.proxy, ) @abstractmethod @@ -45,7 +51,7 @@ def create_session( timeout: Union[int, float, Timeout], verify: bool = True, proxy: Optional[str] = None, - ) -> Union[SyncClient, AsyncClient]: + ) -> Union[Client, AsyncClient]: raise NotImplementedError() def auth( diff --git a/postgrest/base_request_builder.py b/postgrest/base_request_builder.py index 36c9fe87..93e1c24a 100644 --- a/postgrest/base_request_builder.py +++ b/postgrest/base_request_builder.py @@ -18,7 +18,7 @@ Union, ) -from httpx import Headers, QueryParams +from httpx import AsyncClient, Client, Headers, QueryParams from httpx import Response as RequestResponse from pydantic import BaseModel @@ -35,7 +35,7 @@ from pydantic import validator as field_validator from .types import CountMethod, Filters, RequestMethod, ReturnMethod -from .utils import AsyncClient, SyncClient, get_origin_and_cast, sanitize_param +from .utils import get_origin_and_cast, sanitize_param class QueryArgs(NamedTuple): @@ -255,7 +255,7 @@ def from_dict(cls: Type[Self], dict: Dict[str, Any]) -> Self: class BaseFilterRequestBuilder(Generic[_ReturnT]): def __init__( self, - session: Union[AsyncClient, SyncClient], + session: Union[AsyncClient, Client], headers: Headers, params: QueryParams, ) -> None: @@ -530,7 +530,7 @@ def match(self: Self, query: Dict[str, Any]) -> Self: class BaseSelectRequestBuilder(BaseFilterRequestBuilder[_ReturnT]): def __init__( self, - session: Union[AsyncClient, SyncClient], + session: Union[AsyncClient, Client], headers: Headers, params: QueryParams, ) -> None: @@ -636,7 +636,7 @@ def range( class BaseRPCRequestBuilder(BaseSelectRequestBuilder[_ReturnT]): def __init__( self, - session: Union[AsyncClient, SyncClient], + session: Union[AsyncClient, Client], headers: Headers, params: QueryParams, ) -> None: diff --git a/postgrest/deprecated_client.py b/postgrest/deprecated_client.py deleted file mode 100644 index 1d7d9722..00000000 --- a/postgrest/deprecated_client.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from deprecation import deprecated - -from ._async.client import AsyncPostgrestClient -from .version import __version__ - - -class Client(AsyncPostgrestClient): - """Alias to PostgrestClient.""" - - @deprecated("0.2.0", "1.0.0", __version__, "Use PostgrestClient instead") - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - -PostgrestClient = Client diff --git a/postgrest/deprecated_get_request_builder.py b/postgrest/deprecated_get_request_builder.py deleted file mode 100644 index 767cacfc..00000000 --- a/postgrest/deprecated_get_request_builder.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from deprecation import deprecated - -from ._async.request_builder import AsyncSelectRequestBuilder -from .version import __version__ - - -class GetRequestBuilder(AsyncSelectRequestBuilder): - """Alias to SelectRequestBuilder.""" - - @deprecated("0.4.0", "1.0.0", __version__, "Use SelectRequestBuilder instead") - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) diff --git a/postgrest/exceptions.py b/postgrest/exceptions.py index 203153ea..d4ef668d 100644 --- a/postgrest/exceptions.py +++ b/postgrest/exceptions.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Any, Dict, Optional from pydantic import BaseModel @@ -34,7 +34,7 @@ class APIError(Exception): details: Optional[str] """The error details.""" - def __init__(self, error: Dict[str, str]) -> None: + def __init__(self, error: Dict[str, Any]) -> None: self._raw_error = error self.message = error.get("message") self.code = error.get("code") diff --git a/postgrest/utils.py b/postgrest/utils.py index d2f3c48e..53e86e7f 100644 --- a/postgrest/utils.py +++ b/postgrest/utils.py @@ -4,13 +4,29 @@ from typing import Any, Type, TypeVar, cast, get_origin from urllib.parse import urlparse +from deprecation import deprecated from httpx import AsyncClient # noqa: F401 from httpx import Client as BaseClient # noqa: F401 +from pydantic import BaseModel + +from .version import __version__ BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" class SyncClient(BaseClient): + @deprecated( + "1.0.2", "1.3.0", __version__, "Use `Client` from the httpx package instead" + ) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @deprecated( + "1.0.2", + "1.3.0", + __version__, + "Use `close` method from `Client` in the httpx package instead", + ) def aclose(self) -> None: self.close() @@ -66,3 +82,17 @@ def is_valid_jwt(value: str) -> bool: return False return True + + +TBaseModel = TypeVar("TBaseModel", bound=BaseModel) + + +def model_validate_json(model: Type[TBaseModel], contents) -> TBaseModel: + """Compatibility layer between pydantic 1 and 2 for parsing an instance + of a BaseModel from varied""" + try: + # pydantic > 2 + return model.model_validate_json(contents) + except AttributeError: + # pydantic < 2 + return model.parse_raw(contents) diff --git a/postgrest/version.py b/postgrest/version.py index bce3e2c3..cccc8755 100644 --- a/postgrest/version.py +++ b/postgrest/version.py @@ -1 +1 @@ -__version__ = "1.0.2" # {x-release-please-version} +__version__ = "1.1.0" # {x-release-please-version} diff --git a/pyproject.toml b/pyproject.toml index 594811cb..f4eb945c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "postgrest" -version = "1.0.2" # {x-release-please-version} +version = "1.1.0" # {x-release-please-version} description = "PostgREST client for Python. This library provides an ORM interface to PostgREST." authors = ["Lương Quang Mạnh ", "Joel Lee ", "Anand", "Oliver Rice", "Andrew Smith "] homepage = "https://github.com/supabase/postgrest-py" @@ -25,14 +25,14 @@ pydantic = ">=1.9,<3.0" strenum = {version = "^0.4.9", python = "<3.11"} [tool.poetry.dev-dependencies] -pytest = "^8.3.5" +pytest = "^8.4.1" flake8 = "^7.2.0" black = "^25.1" isort = "^6.0.1" pre-commit = "^4.2.0" -pytest-cov = "^6.1.1" +pytest-cov = "^6.2.1" pytest-depends = "^1.0.1" -pytest-asyncio = "^0.26.0" +pytest-asyncio = "^1.0.0" unasync-cli = { git = "https://github.com/supabase-community/unasync-cli.git", branch = "main" } [tool.poetry.group.docs] @@ -44,6 +44,9 @@ furo = ">=2023.9.10,<2025.0.0" [tool.pytest.ini_options] asyncio_mode = "auto" +filterwarnings = [ + "ignore::DeprecationWarning", # ignore deprecation warnings globally +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/_async/client.py b/tests/_async/client.py index cb97e6d0..25cdeb0e 100644 --- a/tests/_async/client.py +++ b/tests/_async/client.py @@ -1,3 +1,5 @@ +from httpx import AsyncClient, AsyncHTTPTransport, Limits + from postgrest import AsyncPostgrestClient REST_URL = "http://127.0.0.1:3000" @@ -7,3 +9,20 @@ def rest_client(): return AsyncPostgrestClient( base_url=REST_URL, ) + + +def rest_client_httpx(): + transport = AsyncHTTPTransport( + retries=4, + limits=Limits( + max_connections=1, + max_keepalive_connections=1, + keepalive_expiry=None, + ), + ) + headers = {"x-user-agent": "my-app/0.0.1"} + http_client = AsyncClient(transport=transport, headers=headers) + return AsyncPostgrestClient( + base_url=REST_URL, + http_client=http_client, + ) diff --git a/tests/_async/test_client.py b/tests/_async/test_client.py index fa32fcaa..6fc0879c 100644 --- a/tests/_async/test_client.py +++ b/tests/_async/test_client.py @@ -1,7 +1,16 @@ from unittest.mock import patch import pytest -from httpx import BasicAuth, Headers, Request, Response +from httpx import ( + AsyncClient, + AsyncHTTPTransport, + BasicAuth, + Headers, + Limits, + Request, + Response, + Timeout, +) from postgrest import AsyncPostgrestClient from postgrest.exceptions import APIError @@ -46,6 +55,32 @@ async def test_custom_headers(self): assert session.headers.items() >= headers.items() +class TestHttpxClientConstructor: + @pytest.mark.asyncio + async def test_custom_httpx_client(self): + transport = AsyncHTTPTransport( + retries=10, + limits=Limits( + max_connections=1, + max_keepalive_connections=1, + keepalive_expiry=None, + ), + ) + headers = {"x-user-agent": "my-app/0.0.1"} + http_client = AsyncClient(transport=transport, headers=headers) + async with AsyncPostgrestClient( + "https://example.com", http_client=http_client, timeout=20.0 + ) as client: + session = client.session + + assert session.base_url == "https://example.com" + assert session.timeout == Timeout( + timeout=5.0 + ) # Should be the default 5 since we use custom httpx client + assert session.headers.get("x-user-agent") == "my-app/0.0.1" + assert isinstance(session, AsyncClient) + + class TestAuth: def test_auth_token(self, postgrest_client: AsyncPostgrestClient): postgrest_client.auth("s3cr3t") diff --git a/tests/_async/test_filter_request_builder.py b/tests/_async/test_filter_request_builder.py index 5a0b837c..a9e92b47 100644 --- a/tests/_async/test_filter_request_builder.py +++ b/tests/_async/test_filter_request_builder.py @@ -1,8 +1,7 @@ import pytest -from httpx import Headers, QueryParams +from httpx import AsyncClient, Headers, QueryParams from postgrest import AsyncFilterRequestBuilder -from postgrest.utils import AsyncClient @pytest.fixture diff --git a/tests/_async/test_filter_request_builder_integration.py b/tests/_async/test_filter_request_builder_integration.py index 26d5260b..b4ff44fc 100644 --- a/tests/_async/test_filter_request_builder_integration.py +++ b/tests/_async/test_filter_request_builder_integration.py @@ -1,11 +1,30 @@ -from .client import rest_client +from postgrest import CountMethod + +from .client import rest_client, rest_client_httpx + + +async def test_multivalued_param_httpx(): + res = ( + await rest_client_httpx() + .from_("countries") + .select("country_name, iso", count=CountMethod.exact) + .lte("numcode", 8) + .gte("numcode", 4) + .execute() + ) + + assert res.count == 2 + assert res.data == [ + {"country_name": "AFGHANISTAN", "iso": "AF"}, + {"country_name": "ALBANIA", "iso": "AL"}, + ] async def test_multivalued_param(): res = ( await rest_client() .from_("countries") - .select("country_name, iso", count="exact") + .select("country_name, iso", count=CountMethod.exact) .lte("numcode", 8) .gte("numcode", 4) .execute() @@ -506,7 +525,12 @@ async def test_rpc_get_with_args(): async def test_rpc_get_with_count(): res = ( await rest_client() - .rpc("search_countries_by_name", {"search_name": "Al"}, get=True, count="exact") + .rpc( + "search_countries_by_name", + {"search_name": "Al"}, + get=True, + count=CountMethod.exact, + ) .select("nicename") .execute() ) @@ -517,7 +541,12 @@ async def test_rpc_get_with_count(): async def test_rpc_head_count(): res = ( await rest_client() - .rpc("search_countries_by_name", {"search_name": "Al"}, head=True, count="exact") + .rpc( + "search_countries_by_name", + {"search_name": "Al"}, + head=True, + count=CountMethod.exact, + ) .execute() ) diff --git a/tests/_async/test_query_request_builder.py b/tests/_async/test_query_request_builder.py index 01c8713f..61fce830 100644 --- a/tests/_async/test_query_request_builder.py +++ b/tests/_async/test_query_request_builder.py @@ -1,8 +1,7 @@ import pytest -from httpx import Headers, QueryParams +from httpx import AsyncClient, Headers, QueryParams from postgrest import AsyncQueryRequestBuilder -from postgrest.utils import AsyncClient @pytest.fixture diff --git a/tests/_async/test_request_builder.py b/tests/_async/test_request_builder.py index 58630dac..1c597e8a 100644 --- a/tests/_async/test_request_builder.py +++ b/tests/_async/test_request_builder.py @@ -1,12 +1,11 @@ from typing import Any, Dict, List import pytest -from httpx import Request, Response +from httpx import AsyncClient, Request, Response from postgrest import AsyncRequestBuilder, AsyncSingleRequestBuilder from postgrest.base_request_builder import APIResponse, SingleAPIResponse from postgrest.types import CountMethod -from postgrest.utils import AsyncClient @pytest.fixture diff --git a/tests/_sync/client.py b/tests/_sync/client.py index 7b3f3e09..a4b2e132 100644 --- a/tests/_sync/client.py +++ b/tests/_sync/client.py @@ -1,3 +1,5 @@ +from httpx import Client, HTTPTransport, Limits + from postgrest import SyncPostgrestClient REST_URL = "http://127.0.0.1:3000" @@ -7,3 +9,20 @@ def rest_client(): return SyncPostgrestClient( base_url=REST_URL, ) + + +def rest_client_httpx(): + transport = HTTPTransport( + retries=4, + limits=Limits( + max_connections=1, + max_keepalive_connections=1, + keepalive_expiry=None, + ), + ) + headers = {"x-user-agent": "my-app/0.0.1"} + http_client = Client(transport=transport, headers=headers) + return SyncPostgrestClient( + base_url=REST_URL, + http_client=http_client, + ) diff --git a/tests/_sync/test_client.py b/tests/_sync/test_client.py index 0e70a63e..d4d17a47 100644 --- a/tests/_sync/test_client.py +++ b/tests/_sync/test_client.py @@ -1,7 +1,16 @@ from unittest.mock import patch import pytest -from httpx import BasicAuth, Headers, Request, Response +from httpx import ( + BasicAuth, + Client, + Headers, + HTTPTransport, + Limits, + Request, + Response, + Timeout, +) from postgrest import SyncPostgrestClient from postgrest.exceptions import APIError @@ -45,6 +54,32 @@ def test_custom_headers(self): assert session.headers.items() >= headers.items() +class TestHttpxClientConstructor: + + def test_custom_httpx_client(self): + transport = HTTPTransport( + retries=10, + limits=Limits( + max_connections=1, + max_keepalive_connections=1, + keepalive_expiry=None, + ), + ) + headers = {"x-user-agent": "my-app/0.0.1"} + http_client = Client(transport=transport, headers=headers) + with SyncPostgrestClient( + "https://example.com", http_client=http_client, timeout=20.0 + ) as client: + session = client.session + + assert session.base_url == "https://example.com" + assert session.timeout == Timeout( + timeout=5.0 + ) # Should be the default 5 since we use custom httpx client + assert session.headers.get("x-user-agent") == "my-app/0.0.1" + assert isinstance(session, Client) + + class TestAuth: def test_auth_token(self, postgrest_client: SyncPostgrestClient): postgrest_client.auth("s3cr3t") diff --git a/tests/_sync/test_filter_request_builder.py b/tests/_sync/test_filter_request_builder.py index aa46b8ed..09b29244 100644 --- a/tests/_sync/test_filter_request_builder.py +++ b/tests/_sync/test_filter_request_builder.py @@ -1,13 +1,12 @@ import pytest -from httpx import Headers, QueryParams +from httpx import Client, Headers, QueryParams from postgrest import SyncFilterRequestBuilder -from postgrest.utils import SyncClient @pytest.fixture def filter_request_builder(): - with SyncClient() as client: + with Client() as client: yield SyncFilterRequestBuilder( client, "/example_table", "GET", Headers(), QueryParams(), {} ) diff --git a/tests/_sync/test_filter_request_builder_integration.py b/tests/_sync/test_filter_request_builder_integration.py index d52fc2ca..896bd9ee 100644 --- a/tests/_sync/test_filter_request_builder_integration.py +++ b/tests/_sync/test_filter_request_builder_integration.py @@ -1,11 +1,30 @@ -from .client import rest_client +from postgrest import CountMethod + +from .client import rest_client, rest_client_httpx + + +def test_multivalued_param_httpx(): + res = ( + rest_client_httpx() + .from_("countries") + .select("country_name, iso", count=CountMethod.exact) + .lte("numcode", 8) + .gte("numcode", 4) + .execute() + ) + + assert res.count == 2 + assert res.data == [ + {"country_name": "AFGHANISTAN", "iso": "AF"}, + {"country_name": "ALBANIA", "iso": "AL"}, + ] def test_multivalued_param(): res = ( rest_client() .from_("countries") - .select("country_name, iso", count="exact") + .select("country_name, iso", count=CountMethod.exact) .lte("numcode", 8) .gte("numcode", 4) .execute() @@ -499,7 +518,12 @@ def test_rpc_get_with_args(): def test_rpc_get_with_count(): res = ( rest_client() - .rpc("search_countries_by_name", {"search_name": "Al"}, get=True, count="exact") + .rpc( + "search_countries_by_name", + {"search_name": "Al"}, + get=True, + count=CountMethod.exact, + ) .select("nicename") .execute() ) @@ -510,7 +534,12 @@ def test_rpc_get_with_count(): def test_rpc_head_count(): res = ( rest_client() - .rpc("search_countries_by_name", {"search_name": "Al"}, head=True, count="exact") + .rpc( + "search_countries_by_name", + {"search_name": "Al"}, + head=True, + count=CountMethod.exact, + ) .execute() ) diff --git a/tests/_sync/test_query_request_builder.py b/tests/_sync/test_query_request_builder.py index 1a5b8ee4..44ce8780 100644 --- a/tests/_sync/test_query_request_builder.py +++ b/tests/_sync/test_query_request_builder.py @@ -1,13 +1,12 @@ import pytest -from httpx import Headers, QueryParams +from httpx import Client, Headers, QueryParams from postgrest import SyncQueryRequestBuilder -from postgrest.utils import SyncClient @pytest.fixture def query_request_builder(): - with SyncClient() as client: + with Client() as client: yield SyncQueryRequestBuilder( client, "/example_table", "GET", Headers(), QueryParams(), {} ) diff --git a/tests/_sync/test_request_builder.py b/tests/_sync/test_request_builder.py index bccd974e..ed53f040 100644 --- a/tests/_sync/test_request_builder.py +++ b/tests/_sync/test_request_builder.py @@ -1,17 +1,16 @@ from typing import Any, Dict, List import pytest -from httpx import Request, Response +from httpx import Client, Request, Response from postgrest import SyncRequestBuilder, SyncSingleRequestBuilder from postgrest.base_request_builder import APIResponse, SingleAPIResponse from postgrest.types import CountMethod -from postgrest.utils import SyncClient @pytest.fixture def request_builder(): - with SyncClient() as client: + with Client() as client: yield SyncRequestBuilder(client, "/example_table") diff --git a/tests/test_utils.py b/tests/test_utils.py index bb6ee9ac..66772925 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,16 @@ import pytest +from deprecation import fail_if_not_removed -from postgrest.utils import sanitize_param +from postgrest.utils import SyncClient, sanitize_param + + +@fail_if_not_removed +def test_sync_client(): + client = SyncClient() + # Verify that aclose method exists and calls close + assert hasattr(client, "aclose") + assert callable(client.aclose) + client.aclose() # Should not raise any exception @pytest.mark.parametrize(