From d10f1a30377586de7a935491342b63e9e987c5b0 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Mon, 12 May 2025 17:21:34 +0000 Subject: [PATCH 1/5] feat: allow injection of httpx client --- supabase_functions/_async/functions_client.py | 25 ++++++++++++------- supabase_functions/_sync/functions_client.py | 25 ++++++++++++------- tests/_async/test_function_client.py | 25 ++++++++++++++++++- tests/_sync/test_function_client.py | 25 ++++++++++++++++++- 4 files changed, 80 insertions(+), 20 deletions(-) diff --git a/supabase_functions/_async/functions_client.py b/supabase_functions/_async/functions_client.py index ba12dbb..a681cb6 100644 --- a/supabase_functions/_async/functions_client.py +++ b/supabase_functions/_async/functions_client.py @@ -22,6 +22,7 @@ def __init__( timeout: int, verify: bool = True, proxy: Optional[str] = None, + http_client: Optional[AsyncClient] = None, ): if not is_http_url(url): raise ValueError("url must be a valid HTTP URL string") @@ -30,15 +31,21 @@ def __init__( "User-Agent": f"supabase-py/functions-py v{__version__}", **headers, } - self._client = AsyncClient( - base_url=self.url, - headers=self.headers, - verify=bool(verify), - timeout=int(abs(timeout)), - proxy=proxy, - follow_redirects=True, - http2=True, - ) + + if http_client is not None: + http_client.base_url = self.url + http_client.headers.update({**self.headers}) + self._client = http_client + else: + self._client = AsyncClient( + base_url=self.url, + headers=self.headers, + verify=bool(verify), + timeout=int(abs(timeout)), + proxy=proxy, + follow_redirects=True, + http2=True, + ) async def _request( self, diff --git a/supabase_functions/_sync/functions_client.py b/supabase_functions/_sync/functions_client.py index d8a410f..bfb3dbf 100644 --- a/supabase_functions/_sync/functions_client.py +++ b/supabase_functions/_sync/functions_client.py @@ -22,6 +22,7 @@ def __init__( timeout: int, verify: bool = True, proxy: Optional[str] = None, + http_client: Optional[SyncClient] = None, ): if not is_http_url(url): raise ValueError("url must be a valid HTTP URL string") @@ -30,15 +31,21 @@ def __init__( "User-Agent": f"supabase-py/functions-py v{__version__}", **headers, } - self._client = SyncClient( - base_url=self.url, - headers=self.headers, - verify=bool(verify), - timeout=int(abs(timeout)), - proxy=proxy, - follow_redirects=True, - http2=True, - ) + + if http_client is not None: + http_client.base_url = self.url + http_client.headers.update({**self.headers}) + self._client = http_client + else: + self._client = SyncClient( + base_url=self.url, + headers=self.headers, + verify=bool(verify), + timeout=int(abs(timeout)), + proxy=proxy, + follow_redirects=True, + http2=True, + ) def _request( self, diff --git a/tests/_async/test_function_client.py b/tests/_async/test_function_client.py index e2ee513..158b6fe 100644 --- a/tests/_async/test_function_client.py +++ b/tests/_async/test_function_client.py @@ -6,7 +6,7 @@ # Import the class to test from supabase_functions import AsyncFunctionsClient from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError -from supabase_functions.utils import FunctionRegion +from supabase_functions.utils import AsyncClient, FunctionRegion from supabase_functions.version import __version__ @@ -197,3 +197,26 @@ async def test_invoke_with_json_body(client: AsyncFunctionsClient): _, kwargs = mock_request.call_args assert kwargs["headers"]["Content-Type"] == "application/json" + + +async def test_init_with_httpx_client(): + # Create a custom httpx client with specific options + custom_client = AsyncClient( + timeout=Timeout(30), follow_redirects=True, max_redirects=5 + ) + + # Initialize the functions client with the custom httpx client + client = AsyncFunctionsClient( + url="https://example.com", + headers={"Authorization": "Bearer token"}, + timeout=30, + http_client=custom_client, + ) + + # Verify the custom client options are preserved + assert client._client.timeout == Timeout(30) + assert client._client.follow_redirects is True + assert client._client.max_redirects == 5 + + # Verify the client is properly configured with our custom client + assert client._client is custom_client diff --git a/tests/_sync/test_function_client.py b/tests/_sync/test_function_client.py index d11bc11..037ea07 100644 --- a/tests/_sync/test_function_client.py +++ b/tests/_sync/test_function_client.py @@ -6,7 +6,7 @@ # Import the class to test from supabase_functions import SyncFunctionsClient from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError -from supabase_functions.utils import FunctionRegion +from supabase_functions.utils import FunctionRegion, SyncClient from supabase_functions.version import __version__ @@ -181,3 +181,26 @@ def test_invoke_with_json_body(client: SyncFunctionsClient): _, kwargs = mock_request.call_args assert kwargs["headers"]["Content-Type"] == "application/json" + + +def test_init_with_httpx_client(): + # Create a custom httpx client with specific options + custom_client = SyncClient( + timeout=Timeout(30), follow_redirects=True, max_redirects=5 + ) + + # Initialize the functions client with the custom httpx client + client = SyncFunctionsClient( + url="https://example.com", + headers={"Authorization": "Bearer token"}, + timeout=30, + http_client=custom_client, + ) + + # Verify the custom client options are preserved + assert client._client.timeout == Timeout(30) + assert client._client.follow_redirects is True + assert client._client.max_redirects == 5 + + # Verify the client is properly configured with our custom client + assert client._client is custom_client From 9a00193b620b6cc06b243c09a4d92357fef657f4 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Wed, 14 May 2025 15:27:33 +0000 Subject: [PATCH 2/5] fix: add deprecation warnings for params to remove in the future --- supabase_functions/_async/functions_client.py | 30 ++++++++++++++++--- supabase_functions/_sync/functions_client.py | 30 ++++++++++++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/supabase_functions/_async/functions_client.py b/supabase_functions/_async/functions_client.py index a681cb6..58035bf 100644 --- a/supabase_functions/_async/functions_client.py +++ b/supabase_functions/_async/functions_client.py @@ -19,8 +19,8 @@ def __init__( self, url: str, headers: Dict, - timeout: int, - verify: bool = True, + timeout: Optional[int] = None, + verify: Optional[bool] = None, proxy: Optional[str] = None, http_client: Optional[AsyncClient] = None, ): @@ -32,6 +32,28 @@ def __init__( **headers, } + if timeout is not None: + warn( + "The 'timeout' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + if verify is not None: + warn( + "The 'verify' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + if proxy is not None: + warn( + "The 'proxy' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.verify = bool(verify) if verify is not None else True + self.timeout = int(abs(timeout)) if timeout is not None else 60 + if http_client is not None: http_client.base_url = self.url http_client.headers.update({**self.headers}) @@ -40,8 +62,8 @@ def __init__( self._client = AsyncClient( base_url=self.url, headers=self.headers, - verify=bool(verify), - timeout=int(abs(timeout)), + verify=self.verify, + timeout=self.timeout, proxy=proxy, follow_redirects=True, http2=True, diff --git a/supabase_functions/_sync/functions_client.py b/supabase_functions/_sync/functions_client.py index bfb3dbf..0750786 100644 --- a/supabase_functions/_sync/functions_client.py +++ b/supabase_functions/_sync/functions_client.py @@ -19,8 +19,8 @@ def __init__( self, url: str, headers: Dict, - timeout: int, - verify: bool = True, + timeout: Optional[int] = None, + verify: Optional[bool] = None, proxy: Optional[str] = None, http_client: Optional[SyncClient] = None, ): @@ -32,6 +32,28 @@ def __init__( **headers, } + if timeout is not None: + warn( + "The 'timeout' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + if verify is not None: + warn( + "The 'verify' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + if proxy is not None: + warn( + "The 'proxy' parameter is deprecated. Please configure it in the httpx client instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.verify = bool(verify) if verify is not None else True + self.timeout = int(abs(timeout)) if timeout is not None else 60 + if http_client is not None: http_client.base_url = self.url http_client.headers.update({**self.headers}) @@ -40,8 +62,8 @@ def __init__( self._client = SyncClient( base_url=self.url, headers=self.headers, - verify=bool(verify), - timeout=int(abs(timeout)), + verify=self.verify, + timeout=self.timeout, proxy=proxy, follow_redirects=True, http2=True, From f540e3703bbfc46e1fb3eb3dad957b88c47dd051 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Wed, 14 May 2025 21:42:21 +0000 Subject: [PATCH 3/5] fix: update deprecation message --- supabase_functions/_async/functions_client.py | 6 +++--- supabase_functions/_sync/functions_client.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/supabase_functions/_async/functions_client.py b/supabase_functions/_async/functions_client.py index 58035bf..72f3bb2 100644 --- a/supabase_functions/_async/functions_client.py +++ b/supabase_functions/_async/functions_client.py @@ -34,19 +34,19 @@ def __init__( if timeout is not None: warn( - "The 'timeout' parameter is deprecated. Please configure it in the httpx client instead.", + "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 httpx client instead.", + "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 httpx client instead.", + "The 'proxy' parameter is deprecated. Please configure it in the http client instead.", DeprecationWarning, stacklevel=2, ) diff --git a/supabase_functions/_sync/functions_client.py b/supabase_functions/_sync/functions_client.py index 0750786..1e607b2 100644 --- a/supabase_functions/_sync/functions_client.py +++ b/supabase_functions/_sync/functions_client.py @@ -34,19 +34,19 @@ def __init__( if timeout is not None: warn( - "The 'timeout' parameter is deprecated. Please configure it in the httpx client instead.", + "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 httpx client instead.", + "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 httpx client instead.", + "The 'proxy' parameter is deprecated. Please configure it in the http client instead.", DeprecationWarning, stacklevel=2, ) From a3fda4a1067fb67245a36ce26ea74e9ec3cb498c Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Sat, 14 Jun 2025 13:25:19 +0100 Subject: [PATCH 4/5] test: add header test and update timeout test --- pyproject.toml | 3 +++ pytest.ini | 2 -- tests/_async/test_function_client.py | 6 ++++-- tests/_sync/test_function_client.py | 6 ++++-- 4 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 pytest.ini diff --git a/pyproject.toml b/pyproject.toml index 08e3d4d..d71fbd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,9 @@ coveralls = "^3.3.0" [tool.pytest.ini_options] asyncio_mode = "auto" addopts = "tests" +filterwarnings = [ + "ignore::DeprecationWarning", # ignore deprecation warnings globally +] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 2f4c80e..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -asyncio_mode = auto diff --git a/tests/_async/test_function_client.py b/tests/_async/test_function_client.py index 158b6fe..e00ee46 100644 --- a/tests/_async/test_function_client.py +++ b/tests/_async/test_function_client.py @@ -201,15 +201,16 @@ async def test_invoke_with_json_body(client: AsyncFunctionsClient): async def test_init_with_httpx_client(): # Create a custom httpx client with specific options + headers = {"x-user-agent": "my-app/0.0.1"} custom_client = AsyncClient( - timeout=Timeout(30), follow_redirects=True, max_redirects=5 + timeout=Timeout(30), follow_redirects=True, max_redirects=5, headers=headers ) # Initialize the functions client with the custom httpx client client = AsyncFunctionsClient( url="https://example.com", headers={"Authorization": "Bearer token"}, - timeout=30, + timeout=10, http_client=custom_client, ) @@ -217,6 +218,7 @@ async def test_init_with_httpx_client(): assert client._client.timeout == Timeout(30) assert client._client.follow_redirects is True assert client._client.max_redirects == 5 + assert client._client.headers.get("x-user-agent") == "my-app/0.0.1" # Verify the client is properly configured with our custom client assert client._client is custom_client diff --git a/tests/_sync/test_function_client.py b/tests/_sync/test_function_client.py index 037ea07..577194f 100644 --- a/tests/_sync/test_function_client.py +++ b/tests/_sync/test_function_client.py @@ -185,15 +185,16 @@ def test_invoke_with_json_body(client: SyncFunctionsClient): def test_init_with_httpx_client(): # Create a custom httpx client with specific options + headers = {"x-user-agent": "my-app/0.0.1"} custom_client = SyncClient( - timeout=Timeout(30), follow_redirects=True, max_redirects=5 + timeout=Timeout(30), follow_redirects=True, max_redirects=5, headers=headers ) # Initialize the functions client with the custom httpx client client = SyncFunctionsClient( url="https://example.com", headers={"Authorization": "Bearer token"}, - timeout=30, + timeout=10, http_client=custom_client, ) @@ -201,6 +202,7 @@ def test_init_with_httpx_client(): assert client._client.timeout == Timeout(30) assert client._client.follow_redirects is True assert client._client.max_redirects == 5 + assert client._client.headers.get("x-user-agent") == "my-app/0.0.1" # Verify the client is properly configured with our custom client assert client._client is custom_client From b5324a5937937939f9e1d0d9ec486ae7eea4d942 Mon Sep 17 00:00:00 2001 From: Andrew Smith Date: Mon, 16 Jun 2025 12:54:19 +0100 Subject: [PATCH 5/5] fix: remove reliance on SyncClient and use Client directly from httpx fix: add deprecation warnings for SyncClient test: update to use Client instead of SyncClient --- Makefile | 1 + supabase_functions/__init__.py | 9 +++++++-- supabase_functions/_async/functions_client.py | 3 +-- supabase_functions/_sync/functions_client.py | 7 +++---- supabase_functions/utils.py | 15 +++++++++++++++ tests/_async/test_function_client.py | 4 ++-- tests/_sync/test_function_client.py | 6 +++--- tests/test_client.py | 6 +++--- 8 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 2d8083f..6ea3a97 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ build_sync: poetry run unasync supabase_functions tests sed -i '0,/SyncMock, /{s/SyncMock, //}' tests/_sync/test_function_client.py sed -i 's/SyncMock/Mock/g' tests/_sync/test_function_client.py + sed -i 's/SyncClient/Client/g' supabase_functions/_sync/functions_client.py tests/_sync/test_function_client.py rename_project: rename_package_dir rename_package diff --git a/supabase_functions/__init__.py b/supabase_functions/__init__.py index 7b81969..7faa952 100644 --- a/supabase_functions/__init__.py +++ b/supabase_functions/__init__.py @@ -1,12 +1,17 @@ from __future__ import annotations -from typing import Literal, Optional, Union, overload +from typing import Literal, Union, overload from ._async.functions_client import AsyncFunctionsClient from ._sync.functions_client import SyncFunctionsClient from .utils import FunctionRegion -__all__ = ["create_client"] +__all__ = [ + "create_client", + "FunctionRegion", + "AsyncFunctionsClient", + "SyncFunctionsClient", +] @overload diff --git a/supabase_functions/_async/functions_client.py b/supabase_functions/_async/functions_client.py index 72f3bb2..439ce3e 100644 --- a/supabase_functions/_async/functions_client.py +++ b/supabase_functions/_async/functions_client.py @@ -1,11 +1,10 @@ from typing import Any, Dict, Literal, Optional, Union from warnings import warn -from httpx import HTTPError, Response +from httpx import AsyncClient, HTTPError, Response from ..errors import FunctionsHttpError, FunctionsRelayError from ..utils import ( - AsyncClient, FunctionRegion, is_http_url, is_valid_jwt, diff --git a/supabase_functions/_sync/functions_client.py b/supabase_functions/_sync/functions_client.py index 1e607b2..cd55fc7 100644 --- a/supabase_functions/_sync/functions_client.py +++ b/supabase_functions/_sync/functions_client.py @@ -1,12 +1,11 @@ from typing import Any, Dict, Literal, Optional, Union from warnings import warn -from httpx import HTTPError, Response +from httpx import Client, HTTPError, Response from ..errors import FunctionsHttpError, FunctionsRelayError from ..utils import ( FunctionRegion, - SyncClient, is_http_url, is_valid_jwt, is_valid_str_arg, @@ -22,7 +21,7 @@ def __init__( timeout: Optional[int] = None, verify: Optional[bool] = None, proxy: Optional[str] = None, - http_client: Optional[SyncClient] = None, + http_client: Optional[Client] = None, ): if not is_http_url(url): raise ValueError("url must be a valid HTTP URL string") @@ -59,7 +58,7 @@ def __init__( http_client.headers.update({**self.headers}) self._client = http_client else: - self._client = SyncClient( + self._client = Client( base_url=self.url, headers=self.headers, verify=self.verify, diff --git a/supabase_functions/utils.py b/supabase_functions/utils.py index e7bf645..6eaf460 100644 --- a/supabase_functions/utils.py +++ b/supabase_functions/utils.py @@ -1,6 +1,7 @@ import re import sys from urllib.parse import urlparse +from warnings import warn from httpx import AsyncClient as AsyncClient # noqa: F401 from httpx import Client as BaseClient @@ -34,7 +35,21 @@ class FunctionRegion(StrEnum): class SyncClient(BaseClient): + def __init__(self, *args, **kwargs): + warn( + "The 'SyncClient' class is deprecated. Please use `Client` from the httpx package instead.", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__(*args, **kwargs) + def aclose(self) -> None: + warn( + "The 'aclose' method is deprecated. Please use `close` method from `Client` in the httpx package instead.", + DeprecationWarning, + stacklevel=2, + ) self.close() diff --git a/tests/_async/test_function_client.py b/tests/_async/test_function_client.py index e00ee46..c2d0650 100644 --- a/tests/_async/test_function_client.py +++ b/tests/_async/test_function_client.py @@ -1,12 +1,12 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from httpx import HTTPError, Response, Timeout +from httpx import AsyncClient, HTTPError, Response, Timeout # Import the class to test from supabase_functions import AsyncFunctionsClient from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError -from supabase_functions.utils import AsyncClient, FunctionRegion +from supabase_functions.utils import FunctionRegion from supabase_functions.version import __version__ diff --git a/tests/_sync/test_function_client.py b/tests/_sync/test_function_client.py index 577194f..f43e11c 100644 --- a/tests/_sync/test_function_client.py +++ b/tests/_sync/test_function_client.py @@ -1,12 +1,12 @@ from unittest.mock import Mock, patch import pytest -from httpx import HTTPError, Response, Timeout +from httpx import Client, HTTPError, Response, Timeout # Import the class to test from supabase_functions import SyncFunctionsClient from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError -from supabase_functions.utils import FunctionRegion, SyncClient +from supabase_functions.utils import FunctionRegion from supabase_functions.version import __version__ @@ -186,7 +186,7 @@ def test_invoke_with_json_body(client: SyncFunctionsClient): def test_init_with_httpx_client(): # Create a custom httpx client with specific options headers = {"x-user-agent": "my-app/0.0.1"} - custom_client = SyncClient( + custom_client = Client( timeout=Timeout(30), follow_redirects=True, max_redirects=5, headers=headers ) diff --git a/tests/test_client.py b/tests/test_client.py index 47f744c..6092909 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -42,8 +42,8 @@ def test_type_hints(): hints = get_type_hints(create_client) - assert hints["url"] == str + assert hints["url"] is str assert hints["headers"] == dict[str, str] - assert hints["is_async"] == bool - assert hints["verify"] == bool + assert hints["is_async"] is bool + assert hints["verify"] is bool assert hints["return"] == Union[AsyncFunctionsClient, SyncFunctionsClient]