diff --git a/src/auth/scripts/run-unasync.py b/src/auth/scripts/run-unasync.py index 0765c518..73f8986b 100644 --- a/src/auth/scripts/run-unasync.py +++ b/src/auth/scripts/run-unasync.py @@ -5,7 +5,14 @@ paths = Path("src/supabase_auth").glob("**/*.py") tests = Path("tests").glob("**/*.py") -rules = (unasync._DEFAULT_RULE,) +rules = ( + unasync.Rule( + fromdir="/_async/", + todir="/_sync/", + additional_replacements={"AsyncClient": "Client", "aclose": "close"}, + ), + unasync._DEFAULT_RULE, +) files = [str(p) for p in list(paths) + list(tests)] diff --git a/src/auth/src/supabase_auth/_async/gotrue_admin_api.py b/src/auth/src/supabase_auth/_async/gotrue_admin_api.py index e2443148..4ffb2415 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_admin_api.py +++ b/src/auth/src/supabase_auth/_async/gotrue_admin_api.py @@ -2,7 +2,7 @@ from typing import Dict, List, Optional -from httpx import QueryParams +from httpx import AsyncClient, QueryParams from ..helpers import ( model_validate, @@ -10,7 +10,6 @@ parse_user_response, validate_uuid, ) -from ..http_clients import AsyncClient from ..types import ( AdminUserAttributes, AuthMFAAdminDeleteFactorParams, diff --git a/src/auth/src/supabase_auth/_async/gotrue_base_api.py b/src/auth/src/supabase_auth/_async/gotrue_base_api.py index 4483495d..ae0bcaa9 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_base_api.py +++ b/src/auth/src/supabase_auth/_async/gotrue_base_api.py @@ -2,13 +2,12 @@ from typing import Any, Dict, Optional -from httpx import HTTPStatusError, QueryParams, Response +from httpx import AsyncClient, HTTPStatusError, QueryParams, Response from pydantic import BaseModel from typing_extensions import Literal, Self from ..constants import API_VERSION_HEADER_NAME, API_VERSIONS_2024_01_01_NAME from ..helpers import handle_exception, model_dump -from ..http_clients import AsyncClient class AsyncGoTrueBaseAPI: diff --git a/src/auth/src/supabase_auth/_async/gotrue_client.py b/src/auth/src/supabase_auth/_async/gotrue_client.py index 3fb71baf..48377282 100644 --- a/src/auth/src/supabase_auth/_async/gotrue_client.py +++ b/src/auth/src/supabase_auth/_async/gotrue_client.py @@ -6,7 +6,7 @@ from urllib.parse import parse_qs, urlparse from uuid import uuid4 -from httpx import QueryParams, Response +from httpx import AsyncClient, QueryParams, Response from jwt import get_algorithm_by_name from typing_extensions import cast @@ -41,7 +41,6 @@ parse_user_response, validate_exp, ) -from ..http_clients import AsyncClient from ..timer import Timer from ..types import ( JWK, diff --git a/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py b/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py index 85857148..dd6d4f02 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py @@ -2,7 +2,7 @@ from typing import Dict, List, Optional -from httpx import QueryParams +from httpx import Client, QueryParams from ..helpers import ( model_validate, @@ -10,7 +10,6 @@ parse_user_response, validate_uuid, ) -from ..http_clients import SyncClient from ..types import ( AdminUserAttributes, AuthMFAAdminDeleteFactorParams, @@ -42,7 +41,7 @@ def __init__( *, url: str = "", headers: Optional[Dict[str, str]] = None, - http_client: Optional[SyncClient] = None, + http_client: Optional[Client] = None, verify: bool = True, proxy: Optional[str] = None, ) -> None: diff --git a/src/auth/src/supabase_auth/_sync/gotrue_base_api.py b/src/auth/src/supabase_auth/_sync/gotrue_base_api.py index 6101ec89..727478eb 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_base_api.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_base_api.py @@ -2,13 +2,12 @@ from typing import Any, Dict, Optional -from httpx import HTTPStatusError, QueryParams, Response +from httpx import Client, HTTPStatusError, QueryParams, Response from pydantic import BaseModel from typing_extensions import Literal, Self from ..constants import API_VERSION_HEADER_NAME, API_VERSIONS_2024_01_01_NAME from ..helpers import handle_exception, model_dump -from ..http_clients import SyncClient class SyncGoTrueBaseAPI: @@ -17,13 +16,13 @@ def __init__( *, url: str, headers: Dict[str, str], - http_client: Optional[SyncClient], + http_client: Optional[Client], verify: bool = True, proxy: Optional[str] = None, ) -> None: self._url = url self._headers = headers - self._http_client = http_client or SyncClient( + self._http_client = http_client or Client( verify=bool(verify), proxy=proxy, follow_redirects=True, @@ -37,7 +36,7 @@ def __exit__(self, exc_t, exc_v, exc_tb) -> None: self.close() def close(self) -> None: - self._http_client.aclose() + self._http_client.close() def _request( self, diff --git a/src/auth/src/supabase_auth/_sync/gotrue_client.py b/src/auth/src/supabase_auth/_sync/gotrue_client.py index cb65fe37..338271f9 100644 --- a/src/auth/src/supabase_auth/_sync/gotrue_client.py +++ b/src/auth/src/supabase_auth/_sync/gotrue_client.py @@ -6,7 +6,7 @@ from urllib.parse import parse_qs, urlparse from uuid import uuid4 -from httpx import QueryParams, Response +from httpx import Client, QueryParams, Response from jwt import get_algorithm_by_name from typing_extensions import cast @@ -41,7 +41,6 @@ parse_user_response, validate_exp, ) -from ..http_clients import SyncClient from ..timer import Timer from ..types import ( JWK, @@ -106,7 +105,7 @@ def __init__( auto_refresh_token: bool = True, persist_session: bool = True, storage: Optional[SyncSupportedStorage] = None, - http_client: Optional[SyncClient] = None, + http_client: Optional[Client] = None, flow_type: AuthFlowType = "implicit", verify: bool = True, proxy: Optional[str] = None, diff --git a/src/auth/src/supabase_auth/http_clients.py b/src/auth/src/supabase_auth/http_clients.py deleted file mode 100644 index 6dbd91d9..00000000 --- a/src/auth/src/supabase_auth/http_clients.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from httpx import AsyncClient # noqa: F401 -from httpx import Client as BaseClient - - -class SyncClient(BaseClient): - def aclose(self) -> None: - self.close() diff --git a/src/postgrest/Makefile b/src/postgrest/Makefile index 2a9ae0a4..9f13a176 100644 --- a/src/postgrest/Makefile +++ b/src/postgrest/Makefile @@ -12,7 +12,7 @@ help:: @echo " pytest -- run pytest on postgrest package" mypy: - uv run --package supabase_functions mypy src/postgrest tests + uv run --package postgrest mypy src/postgrest tests help:: @echo " mypy -- run mypy on postgrest package" @@ -60,4 +60,4 @@ help:: build: uv build --package postgrest help:: - @echo " build -- invoke uv build on storage3 package" + @echo " build -- invoke uv build on postgrest package" diff --git a/src/realtime/src/realtime/types.py b/src/realtime/src/realtime/types.py index feaca122..1223f06a 100644 --- a/src/realtime/src/realtime/types.py +++ b/src/realtime/src/realtime/types.py @@ -201,7 +201,7 @@ class RealtimeChannelConfig(TypedDict): class RealtimeChannelOptions(TypedDict): - config: RealtimeChannelConfig + config: NotRequired[RealtimeChannelConfig] @with_config(ConfigDict(extra="allow")) diff --git a/src/supabase/Makefile b/src/supabase/Makefile index 18834a0d..811678ba 100644 --- a/src/supabase/Makefile +++ b/src/supabase/Makefile @@ -1,7 +1,23 @@ .PHONY: pytest pre-commit unasync build-sync tests +help:: + @echo "Available commands" + @echo " help -- (default) print this message" + +tests: pytest +help:: + @echo " tests -- run all tests for supabase package" + pytest: uv run --package supabase pytest --cov=./ --cov-report=xml --cov-report=html -vv +help:: + @echo " pytest -- run pytest on supabase package" + +mypy: + uv run --package supabase mypy src/supabase tests +help:: + @echo " mypy -- run mypy on supabase package" + unasync: uv run run-unasync.py @@ -14,12 +30,16 @@ build-sync: unasync sed -i 's/SyncClient/Client/gi' tests/_sync/test_client.py sed -i 's/SyncHTTPTransport/HTTPTransport/g' tests/_sync/test_client.py sed -i 's/SyncMock/Mock/g' tests/_sync/test_client.py - -tests: pytest +help:: + @echo " build-sync -- generate _sync from _async implementation" clean: rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache rm -f .coverage coverage.xml +help:: + @echo " clean -- clean intermediary files generated by tests" build: uv build --package supabase +help:: + @echo " build -- invoke uv build on supabase package" diff --git a/src/supabase/pyproject.toml b/src/supabase/pyproject.toml index baf7ed04..cc605319 100644 --- a/src/supabase/pyproject.toml +++ b/src/supabase/pyproject.toml @@ -55,6 +55,25 @@ lints = [ [tool.uv] default-groups = [ "dev" ] +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + # "SIM", + # isort + "I", + "ANN2" +] +ignore = ["F403", "E501", "E402", "UP006", "UP035"] + + [build-system] requires = ["uv_build>=0.8.3,<0.9.0"] build-backend = "uv_build" diff --git a/src/supabase/run-unasync.py b/src/supabase/run-unasync.py index 3811899e..d3094584 100644 --- a/src/supabase/run-unasync.py +++ b/src/supabase/run-unasync.py @@ -1,10 +1,18 @@ -import unasync from pathlib import Path +import unasync + paths = Path("src/supabase").glob("**/*.py") tests = Path("tests").glob("**/*.py") -rules = (unasync._DEFAULT_RULE,) +rules = ( + unasync.Rule( + fromdir="/_async/", + todir="/_sync/", + additional_replacements={"AsyncClient": "Client"}, + ), + unasync._DEFAULT_RULE, +) files = [str(p) for p in list(paths) + list(tests)] diff --git a/src/supabase/src/supabase/__init__.py b/src/supabase/src/supabase/__init__.py index 37d07cc7..2abfed2b 100644 --- a/src/supabase/src/supabase/__init__.py +++ b/src/supabase/src/supabase/__init__.py @@ -1,3 +1,7 @@ +from postgrest import APIError as PostgrestAPIError +from postgrest import APIResponse as PostgrestAPIResponse +from realtime import AuthorizationError, NotConnectedError +from storage3.utils import StorageException from supabase_auth.errors import ( AuthApiError, AuthError, @@ -8,10 +12,6 @@ AuthUnknownError, AuthWeakPasswordError, ) -from postgrest import APIError as PostgrestAPIError -from postgrest import APIResponse as PostgrestAPIResponse -from realtime import AuthorizationError, NotConnectedError -from storage3.utils import StorageException from supabase_functions.errors import ( FunctionsError, FunctionsHttpError, @@ -30,11 +30,9 @@ # Sync Client from ._sync.auth_client import SyncSupabaseAuthClient as SupabaseAuthClient -from ._sync.client import SupabaseException +from ._sync.client import Client, SupabaseException, create_client from ._sync.client import SupabaseException as SyncSupabaseException -from ._sync.client import SyncClient as Client from ._sync.client import SyncStorageClient as SupabaseStorageClient -from ._sync.client import create_client # Lib from .lib.client_options import AsyncClientOptions diff --git a/src/supabase/src/supabase/_async/auth_client.py b/src/supabase/src/supabase/_async/auth_client.py index 9353a903..6f7ff5ea 100644 --- a/src/supabase/src/supabase/_async/auth_client.py +++ b/src/supabase/src/supabase/_async/auth_client.py @@ -1,12 +1,11 @@ from typing import Dict, Optional +from httpx import AsyncClient from supabase_auth import ( AsyncGoTrueClient, - AsyncMemoryStorage, AsyncSupportedStorage, AuthFlowType, ) -from supabase_auth.http_clients import AsyncClient class AsyncSupabaseAuthClient(AsyncGoTrueClient): @@ -20,12 +19,12 @@ def __init__( storage_key: Optional[str] = None, auto_refresh_token: bool = True, persist_session: bool = True, - storage: AsyncSupportedStorage = AsyncMemoryStorage(), + storage: Optional[AsyncSupportedStorage] = None, http_client: Optional[AsyncClient] = None, flow_type: AuthFlowType = "implicit", verify: bool = True, proxy: Optional[str] = None, - ): + ) -> None: """ Instantiate a SupabaseAuthClient instance. diff --git a/src/supabase/src/supabase/_async/client.py b/src/supabase/src/supabase/_async/client.py index ab4ee585..4fa5c0b4 100644 --- a/src/supabase/src/supabase/_async/client.py +++ b/src/supabase/src/supabase/_async/client.py @@ -1,29 +1,32 @@ import asyncio import copy import re -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union -from supabase_auth import AsyncMemoryStorage -from supabase_auth.types import AuthChangeEvent, Session from httpx import Timeout from postgrest import ( AsyncPostgrestClient, + AsyncRequestBuilder, + AsyncRPCFilterRequestBuilder, ) from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT from postgrest.types import CountMethod from realtime import AsyncRealtimeChannel, AsyncRealtimeClient, RealtimeChannelOptions from storage3 import AsyncStorageClient from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT +from supabase_auth import AsyncMemoryStorage +from supabase_auth.types import AuthChangeEvent, Session from supabase_functions import AsyncFunctionsClient from ..lib.client_options import AsyncClientOptions as ClientOptions from ..lib.client_options import AsyncHttpxClient +from ..types import RealtimeClientOptions from .auth_client import AsyncSupabaseAuthClient # Create an exception class when user does not provide a valid url or key. class SupabaseException(Exception): - def __init__(self, message: str): + def __init__(self, message: str) -> None: self.message = message super().__init__(self.message) @@ -36,7 +39,7 @@ def __init__( supabase_url: str, supabase_key: str, options: Optional[ClientOptions] = None, - ): + ) -> None: """Instantiate the client. Parameters @@ -86,9 +89,9 @@ def __init__( supabase_key=self.supabase_key, options=self.options.realtime if self.options else None, ) - self._postgrest = None - self._storage = None - self._functions = None + self._postgrest: Optional[AsyncPostgrestClient] = None + self._storage: Optional[AsyncStorageClient] = None + self._functions: Optional[AsyncFunctionsClient] = None self.auth.on_auth_state_change(self._listen_to_auth_events) @classmethod @@ -97,14 +100,18 @@ async def create( supabase_url: str, supabase_key: str, options: Optional[ClientOptions] = None, - ): + ) -> "AsyncClient": auth_header = options.headers.get("Authorization") if options else None client = cls(supabase_url, supabase_key, options) if auth_header is None: try: session = await client.auth.get_session() - session_access_token = client._create_auth_header(session.access_token) + session_access_token = ( + client._create_auth_header(session.access_token) + if session + else None + ) except Exception: session_access_token = None @@ -114,7 +121,7 @@ async def create( return client - def table(self, table_name: str): + def table(self, table_name: str) -> AsyncRequestBuilder: """Perform a table operation. Note that the supabase client uses the `from` method, but in Python, @@ -123,14 +130,14 @@ def table(self, table_name: str): """ return self.from_(table_name) - def schema(self, schema: str): + def schema(self, schema: str) -> AsyncPostgrestClient: """Select a schema to query or perform an function (rpc) call. The schema needs to be on the list of exposed schemas inside Supabase. """ return self.postgrest.schema(schema) - def from_(self, table_name: str): + def from_(self, table_name: str) -> AsyncRequestBuilder: """Perform a table operation. See the `table` method. @@ -144,7 +151,7 @@ def rpc( count: Optional[CountMethod] = None, head: bool = False, get: bool = False, - ): + ) -> AsyncRPCFilterRequestBuilder: """Performs a stored procedure call. Parameters @@ -168,7 +175,7 @@ def rpc( return self.postgrest.rpc(fn, params, count, head, get) @property - def postgrest(self): + def postgrest(self) -> AsyncPostgrestClient: if self._postgrest is None: self._postgrest = self._init_postgrest_client( rest_url=self.rest_url, @@ -181,7 +188,7 @@ def postgrest(self): return self._postgrest @property - def storage(self): + def storage(self) -> AsyncStorageClient: if self._storage is None: self._storage = self._init_storage_client( storage_url=self.storage_url, @@ -192,7 +199,7 @@ def storage(self): return self._storage @property - def functions(self): + def functions(self) -> AsyncFunctionsClient: if self._functions is None: self._functions = AsyncFunctionsClient( url=self.functions_url, @@ -207,31 +214,32 @@ def functions(self): return self._functions def channel( - self, topic: str, params: RealtimeChannelOptions = {} + self, topic: str, params: Optional[RealtimeChannelOptions] = None ) -> AsyncRealtimeChannel: """Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.""" - return self.realtime.channel(topic, params) + return self.realtime.channel(topic, params or {}) - def get_channels(self): + def get_channels(self) -> List[AsyncRealtimeChannel]: """Returns all realtime channels.""" return self.realtime.get_channels() - async def remove_channel(self, channel: AsyncRealtimeChannel): + async def remove_channel(self, channel: AsyncRealtimeChannel) -> None: """Unsubscribes and removes Realtime channel from Realtime client.""" await self.realtime.remove_channel(channel) - async def remove_all_channels(self): + async def remove_all_channels(self) -> None: """Unsubscribes and removes all Realtime channels from Realtime client.""" await self.realtime.remove_all_channels() @staticmethod def _init_realtime_client( - realtime_url: str, supabase_key: str, options: Optional[Dict[str, Any]] = None + realtime_url: str, + supabase_key: str, + options: Optional[RealtimeClientOptions] = None, ) -> AsyncRealtimeClient: - if options is None: - options = {} + realtime_options = options or {} """Private method for creating an instance of the realtime-py client.""" - return AsyncRealtimeClient(realtime_url, token=supabase_key, **options) + return AsyncRealtimeClient(realtime_url, token=supabase_key, **realtime_options) @staticmethod def _init_storage_client( @@ -244,19 +252,16 @@ def _init_storage_client( ) -> AsyncStorageClient: if http_client is not None: # If an http client is provided, use it - kwargs = {"http_client": http_client} - else: - kwargs = { - "timeout": storage_client_timeout, - "verify": verify, - "proxy": proxy, - "http_client": None, - } - + return AsyncStorageClient( + url=storage_url, headers=headers, http_client=http_client + ) return AsyncStorageClient( url=storage_url, headers=headers, - **kwargs, + timeout=storage_client_timeout, + verify=verify, + proxy=proxy, + http_client=None, ) @staticmethod @@ -292,23 +297,20 @@ def _init_postgrest_client( """Private helper for creating an instance of the Postgrest client.""" if http_client is not None: # If an http client is provided, use it - kwargs = {"http_client": http_client} - else: - kwargs = { - "timeout": timeout, - "verify": verify, - "proxy": proxy, - "http_client": None, - } - + return AsyncPostgrestClient( + rest_url, headers=headers, schema=schema, http_client=http_client + ) return AsyncPostgrestClient( rest_url, headers=headers, schema=schema, - **kwargs, + timeout=timeout, + verify=verify, + proxy=proxy, + http_client=None, ) - def _create_auth_header(self, token: str): + def _create_auth_header(self, token: str) -> str: return f"Bearer {token}" def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: @@ -325,7 +327,7 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] - ): + ) -> None: access_token = self.supabase_key if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change diff --git a/src/supabase/src/supabase/_sync/auth_client.py b/src/supabase/src/supabase/_sync/auth_client.py index 12f259cd..9b701241 100644 --- a/src/supabase/src/supabase/_sync/auth_client.py +++ b/src/supabase/src/supabase/_sync/auth_client.py @@ -1,12 +1,11 @@ from typing import Dict, Optional +from httpx import Client from supabase_auth import ( + AuthFlowType, SyncGoTrueClient, - SyncMemoryStorage, SyncSupportedStorage, - AuthFlowType, ) -from supabase_auth.http_clients import SyncClient class SyncSupabaseAuthClient(SyncGoTrueClient): @@ -20,12 +19,12 @@ def __init__( storage_key: Optional[str] = None, auto_refresh_token: bool = True, persist_session: bool = True, - storage: SyncSupportedStorage = SyncMemoryStorage(), - http_client: Optional[SyncClient] = None, + storage: Optional[SyncSupportedStorage] = None, + http_client: Optional[Client] = None, flow_type: AuthFlowType = "implicit", verify: bool = True, proxy: Optional[str] = None, - ): + ) -> None: """ Instantiate a SupabaseAuthClient instance. diff --git a/src/supabase/src/supabase/_sync/client.py b/src/supabase/src/supabase/_sync/client.py index 7e6bb376..702b31fa 100644 --- a/src/supabase/src/supabase/_sync/client.py +++ b/src/supabase/src/supabase/_sync/client.py @@ -1,33 +1,36 @@ import copy import re -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union -from supabase_auth import SyncMemoryStorage -from supabase_auth.types import AuthChangeEvent, Session from httpx import Timeout from postgrest import ( SyncPostgrestClient, + SyncRequestBuilder, + SyncRPCFilterRequestBuilder, ) from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT from postgrest.types import CountMethod -from realtime import SyncRealtimeChannel, SyncRealtimeClient, RealtimeChannelOptions +from realtime import RealtimeChannelOptions, SyncRealtimeChannel, SyncRealtimeClient from storage3 import SyncStorageClient from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT +from supabase_auth import SyncMemoryStorage +from supabase_auth.types import AuthChangeEvent, Session from supabase_functions import SyncFunctionsClient from ..lib.client_options import SyncClientOptions as ClientOptions from ..lib.client_options import SyncHttpxClient +from ..types import RealtimeClientOptions from .auth_client import SyncSupabaseAuthClient # Create an exception class when user does not provide a valid url or key. class SupabaseException(Exception): - def __init__(self, message: str): + def __init__(self, message: str) -> None: self.message = message super().__init__(self.message) -class SyncClient: +class Client: """Supabase client class.""" def __init__( @@ -35,7 +38,7 @@ def __init__( supabase_url: str, supabase_key: str, options: Optional[ClientOptions] = None, - ): + ) -> None: """Instantiate the client. Parameters @@ -85,9 +88,9 @@ def __init__( supabase_key=self.supabase_key, options=self.options.realtime if self.options else None, ) - self._postgrest = None - self._storage = None - self._functions = None + self._postgrest: Optional[SyncPostgrestClient] = None + self._storage: Optional[SyncStorageClient] = None + self._functions: Optional[SyncFunctionsClient] = None self.auth.on_auth_state_change(self._listen_to_auth_events) @classmethod @@ -96,14 +99,18 @@ def create( supabase_url: str, supabase_key: str, options: Optional[ClientOptions] = None, - ): + ) -> "Client": auth_header = options.headers.get("Authorization") if options else None client = cls(supabase_url, supabase_key, options) if auth_header is None: try: session = client.auth.get_session() - session_access_token = client._create_auth_header(session.access_token) + session_access_token = ( + client._create_auth_header(session.access_token) + if session + else None + ) except Exception: session_access_token = None @@ -113,7 +120,7 @@ def create( return client - def table(self, table_name: str): + def table(self, table_name: str) -> SyncRequestBuilder: """Perform a table operation. Note that the supabase client uses the `from` method, but in Python, @@ -122,14 +129,14 @@ def table(self, table_name: str): """ return self.from_(table_name) - def schema(self, schema: str): + def schema(self, schema: str) -> SyncPostgrestClient: """Select a schema to query or perform an function (rpc) call. The schema needs to be on the list of exposed schemas inside Supabase. """ return self.postgrest.schema(schema) - def from_(self, table_name: str): + def from_(self, table_name: str) -> SyncRequestBuilder: """Perform a table operation. See the `table` method. @@ -143,7 +150,7 @@ def rpc( count: Optional[CountMethod] = None, head: bool = False, get: bool = False, - ): + ) -> SyncRPCFilterRequestBuilder: """Performs a stored procedure call. Parameters @@ -167,7 +174,7 @@ def rpc( return self.postgrest.rpc(fn, params, count, head, get) @property - def postgrest(self): + def postgrest(self) -> SyncPostgrestClient: if self._postgrest is None: self._postgrest = self._init_postgrest_client( rest_url=self.rest_url, @@ -180,7 +187,7 @@ def postgrest(self): return self._postgrest @property - def storage(self): + def storage(self) -> SyncStorageClient: if self._storage is None: self._storage = self._init_storage_client( storage_url=self.storage_url, @@ -191,7 +198,7 @@ def storage(self): return self._storage @property - def functions(self): + def functions(self) -> SyncFunctionsClient: if self._functions is None: self._functions = SyncFunctionsClient( url=self.functions_url, @@ -206,31 +213,32 @@ def functions(self): return self._functions def channel( - self, topic: str, params: RealtimeChannelOptions = {} + self, topic: str, params: Optional[RealtimeChannelOptions] = None ) -> SyncRealtimeChannel: """Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.""" - return self.realtime.channel(topic, params) + return self.realtime.channel(topic, params or {}) - def get_channels(self): + def get_channels(self) -> List[SyncRealtimeChannel]: """Returns all realtime channels.""" return self.realtime.get_channels() - def remove_channel(self, channel: SyncRealtimeChannel): + def remove_channel(self, channel: SyncRealtimeChannel) -> None: """Unsubscribes and removes Realtime channel from Realtime client.""" self.realtime.remove_channel(channel) - def remove_all_channels(self): + def remove_all_channels(self) -> None: """Unsubscribes and removes all Realtime channels from Realtime client.""" self.realtime.remove_all_channels() @staticmethod def _init_realtime_client( - realtime_url: str, supabase_key: str, options: Optional[Dict[str, Any]] = None + realtime_url: str, + supabase_key: str, + options: Optional[RealtimeClientOptions] = None, ) -> SyncRealtimeClient: - if options is None: - options = {} + realtime_options = options or {} """Private method for creating an instance of the realtime-py client.""" - return SyncRealtimeClient(realtime_url, token=supabase_key, **options) + return SyncRealtimeClient(realtime_url, token=supabase_key, **realtime_options) @staticmethod def _init_storage_client( @@ -243,19 +251,16 @@ def _init_storage_client( ) -> SyncStorageClient: if http_client is not None: # If an http client is provided, use it - kwargs = {"http_client": http_client} - else: - kwargs = { - "timeout": storage_client_timeout, - "verify": verify, - "proxy": proxy, - "http_client": None, - } - + return SyncStorageClient( + url=storage_url, headers=headers, http_client=http_client + ) return SyncStorageClient( url=storage_url, headers=headers, - **kwargs, + timeout=storage_client_timeout, + verify=verify, + proxy=proxy, + http_client=None, ) @staticmethod @@ -291,23 +296,20 @@ def _init_postgrest_client( """Private helper for creating an instance of the Postgrest client.""" if http_client is not None: # If an http client is provided, use it - kwargs = {"http_client": http_client} - else: - kwargs = { - "timeout": timeout, - "verify": verify, - "proxy": proxy, - "http_client": None, - } - + return SyncPostgrestClient( + rest_url, headers=headers, schema=schema, http_client=http_client + ) return SyncPostgrestClient( rest_url, headers=headers, schema=schema, - **kwargs, + timeout=timeout, + verify=verify, + proxy=proxy, + http_client=None, ) - def _create_auth_header(self, token: str): + def _create_auth_header(self, token: str) -> str: return f"Bearer {token}" def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, str]: @@ -324,7 +326,7 @@ def _get_auth_headers(self, authorization: Optional[str] = None) -> Dict[str, st def _listen_to_auth_events( self, event: AuthChangeEvent, session: Optional[Session] - ): + ) -> None: access_token = self.supabase_key if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change @@ -339,7 +341,7 @@ def create_client( supabase_url: str, supabase_key: str, options: Optional[ClientOptions] = None, -) -> SyncClient: +) -> Client: """Create client function to instantiate supabase client like JS runtime. Parameters @@ -366,6 +368,6 @@ def create_client( ------- Client """ - return SyncClient.create( + return Client.create( supabase_url=supabase_url, supabase_key=supabase_key, options=options ) diff --git a/src/supabase/src/supabase/client.py b/src/supabase/src/supabase/client.py index 87eca6d2..edd00387 100644 --- a/src/supabase/src/supabase/client.py +++ b/src/supabase/src/supabase/client.py @@ -1,3 +1,7 @@ +from postgrest import APIError as PostgrestAPIError +from postgrest import APIResponse as PostgrestAPIResponse +from realtime import AuthorizationError, NotConnectedError +from storage3.utils import StorageException from supabase_auth.errors import ( AuthApiError, AuthError, @@ -8,10 +12,6 @@ AuthUnknownError, AuthWeakPasswordError, ) -from postgrest import APIError as PostgrestAPIError -from postgrest import APIResponse as PostgrestAPIResponse -from realtime import AuthorizationError, NotConnectedError -from storage3.utils import StorageException from supabase_functions.errors import ( FunctionsError, FunctionsHttpError, @@ -27,9 +27,8 @@ # Sync Client from ._sync.auth_client import SyncSupabaseAuthClient as SupabaseAuthClient -from ._sync.client import SyncClient as Client +from ._sync.client import Client, create_client from ._sync.client import SyncStorageClient as SupabaseStorageClient -from ._sync.client import create_client # Lib from .lib.client_options import AsyncClientOptions diff --git a/src/supabase/src/supabase/lib/client_options.py b/src/supabase/src/supabase/lib/client_options.py index ab38c3b4..44450c0e 100644 --- a/src/supabase/src/supabase/lib/client_options.py +++ b/src/supabase/src/supabase/lib/client_options.py @@ -1,6 +1,11 @@ from dataclasses import dataclass, field from typing import Dict, Optional, Union +from httpx import AsyncClient as AsyncHttpxClient +from httpx import Client as SyncHttpxClient +from httpx import Timeout +from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT +from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supabase_auth import ( AsyncMemoryStorage, AsyncSupportedStorage, @@ -8,11 +13,6 @@ SyncMemoryStorage, SyncSupportedStorage, ) -from httpx import AsyncClient as AsyncHttpxClient -from httpx import Client as SyncHttpxClient -from httpx import Timeout -from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT -from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supabase_functions.utils import DEFAULT_FUNCTION_CLIENT_TIMEOUT from supabase.types import RealtimeClientOptions @@ -39,68 +39,23 @@ class ClientOptions: persist_session: bool = True """Whether to persist a logged in session to storage.""" - storage: SyncSupportedStorage = field(default_factory=SyncMemoryStorage) - """A storage provider. Used to store the logged in session.""" - realtime: Optional[RealtimeClientOptions] = None """Options passed to the realtime-py instance""" - httpx_client: Optional[SyncHttpxClient] = None - """httpx client instance to be used by the PostgREST, functions, auth and storage clients.""" - postgrest_client_timeout: Union[int, float, Timeout] = ( DEFAULT_POSTGREST_CLIENT_TIMEOUT ) """Timeout passed to the SyncPostgrestClient instance.""" - storage_client_timeout: Union[int, float, Timeout] = DEFAULT_STORAGE_CLIENT_TIMEOUT + storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT """Timeout passed to the SyncStorageClient instance""" - function_client_timeout: Union[int, float, Timeout] = ( - DEFAULT_FUNCTION_CLIENT_TIMEOUT - ) + function_client_timeout: int = DEFAULT_FUNCTION_CLIENT_TIMEOUT """Timeout passed to the SyncFunctionsClient instance.""" flow_type: AuthFlowType = "pkce" """flow type to use for authentication""" - def replace( - self, - schema: Optional[str] = None, - headers: Optional[Dict[str, str]] = None, - auto_refresh_token: Optional[bool] = None, - persist_session: Optional[bool] = None, - storage: Optional[SyncSupportedStorage] = None, - realtime: Optional[RealtimeClientOptions] = None, - httpx_client: Optional[SyncHttpxClient] = None, - postgrest_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - storage_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, - flow_type: Optional[AuthFlowType] = None, - ) -> "ClientOptions": - """Create a new SupabaseClientOptions with changes""" - client_options = ClientOptions() - client_options.schema = schema or self.schema - client_options.headers = headers or self.headers - client_options.auto_refresh_token = ( - auto_refresh_token or self.auto_refresh_token - ) - client_options.persist_session = persist_session or self.persist_session - client_options.storage = storage or self.storage - client_options.realtime = realtime or self.realtime - client_options.httpx_client = httpx_client or self.httpx_client - client_options.postgrest_client_timeout = ( - postgrest_client_timeout or self.postgrest_client_timeout - ) - client_options.storage_client_timeout = ( - storage_client_timeout or self.storage_client_timeout - ) - client_options.flow_type = flow_type or self.flow_type - return client_options - @dataclass class AsyncClientOptions(ClientOptions): @@ -122,9 +77,7 @@ def replace( postgrest_client_timeout: Union[ int, float, Timeout ] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - storage_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, + storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, ) -> "AsyncClientOptions": """Create a new SupabaseClientOptions with changes""" @@ -150,6 +103,11 @@ def replace( @dataclass class SyncClientOptions(ClientOptions): + storage: SyncSupportedStorage = field(default_factory=SyncMemoryStorage) + """A storage provider. Used to store the logged in session.""" + httpx_client: Optional[SyncHttpxClient] = None + """httpx client instance to be used by the PostgREST, functions, auth and storage clients.""" + def replace( self, schema: Optional[str] = None, @@ -162,9 +120,7 @@ def replace( postgrest_client_timeout: Union[ int, float, Timeout ] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - storage_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, + storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, ) -> "SyncClientOptions": """Create a new SupabaseClientOptions with changes""" diff --git a/src/supabase/tests/_async/test_client.py b/src/supabase/tests/_async/test_client.py index 6bd28dff..f3423ee6 100644 --- a/src/supabase/tests/_async/test_client.py +++ b/src/supabase/tests/_async/test_client.py @@ -3,9 +3,9 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from supabase_auth import AsyncMemoryStorage from httpx import AsyncClient as AsyncHttpxClient from httpx import AsyncHTTPTransport, Limits, Timeout +from supabase_auth import AsyncMemoryStorage from supabase import ( AsyncClient, @@ -36,8 +36,8 @@ async def test_supabase_exception() -> None: async def test_postgrest_client() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) assert client.table("sample") @@ -45,24 +45,24 @@ async def test_postgrest_client() -> None: async def test_rpc_client() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) assert client.rpc("test_fn") async def test_function_initialization() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) assert client.functions async def test_uses_key_as_authorization_header_by_default() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) @@ -80,8 +80,8 @@ async def test_uses_key_as_authorization_header_by_default() -> None: async def test_schema_update() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) assert client.postgrest @@ -89,8 +89,8 @@ async def test_schema_update() -> None: async def test_updates_the_authorization_header_on_auth_events() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = await create_async_client(url, key) @@ -121,8 +121,8 @@ async def test_updates_the_authorization_header_on_auth_events() -> None: async def test_supports_setting_a_global_authorization_header() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] authorization = "Bearer secretuserjwt" @@ -143,9 +143,9 @@ async def test_supports_setting_a_global_authorization_header() -> None: assert client.storage.session.headers.get("Authorization") == authorization -async def test_mutable_headers_issue(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +async def test_mutable_headers_issue() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] shared_options = AsyncClientOptions( storage=AsyncMemoryStorage(), headers={"Authorization": "Bearer initial-token"} @@ -160,9 +160,9 @@ async def test_mutable_headers_issue(): assert client1.options.headers["Authorization"] == "Bearer modified-token" -async def test_global_authorization_header_issue(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +async def test_global_authorization_header_issue() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] authorization = "Bearer secretuserjwt" options = AsyncClientOptions(headers={"Authorization": authorization}) @@ -172,9 +172,9 @@ async def test_global_authorization_header_issue(): assert client.options.headers.get("apiKey") == key -async def test_httpx_client(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +async def test_httpx_client() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] transport = AsyncHTTPTransport( retries=10, @@ -203,9 +203,9 @@ async def test_httpx_client(): assert client.functions._client.timeout == Timeout(2.0) -async def test_custom_headers(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +async def test_custom_headers() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] options = AsyncClientOptions( headers={ @@ -220,9 +220,9 @@ async def test_custom_headers(): assert client.options.headers.get("x-version") == "1.0" -async def test_custom_headers_immutable(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +async def test_custom_headers_immutable() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] options = AsyncClientOptions( headers={ @@ -241,14 +241,14 @@ async def test_custom_headers_immutable(): assert client2.options.headers.get("x-app-name") == "apple" -async def test_httpx_client_base_url_isolation(): +async def test_httpx_client_base_url_isolation() -> None: """Test that shared httpx_client doesn't cause base_url mutation between services. This test reproduces the issue where accessing PostgREST after Storage causes Storage requests to hit the wrong endpoint (404 errors). See: https://github.com/supabase/supabase-py/issues/1244 """ - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] # Create client with shared httpx instance timeout = Timeout(10.0, read=60.0) diff --git a/src/supabase/tests/_sync/test_client.py b/src/supabase/tests/_sync/test_client.py index e4123348..a490d67d 100644 --- a/src/supabase/tests/_sync/test_client.py +++ b/src/supabase/tests/_sync/test_client.py @@ -1,11 +1,11 @@ import os from typing import Any -from unittest.mock import Mock, MagicMock +from unittest.mock import MagicMock, Mock import pytest -from supabase_auth import SyncMemoryStorage from httpx import Client as SyncHttpxClient from httpx import HTTPTransport, Limits, Timeout +from supabase_auth import SyncMemoryStorage from supabase import ( Client, @@ -36,8 +36,8 @@ def test_supabase_exception() -> None: def test_postgrest_client() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) assert client.table("sample") @@ -45,24 +45,24 @@ def test_postgrest_client() -> None: def test_rpc_client() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) assert client.rpc("test_fn") def test_function_initialization() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) assert client.functions def test_uses_key_as_authorization_header_by_default() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) @@ -80,8 +80,8 @@ def test_uses_key_as_authorization_header_by_default() -> None: def test_schema_update() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) assert client.postgrest @@ -89,8 +89,8 @@ def test_schema_update() -> None: def test_updates_the_authorization_header_on_auth_events() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] client = create_client(url, key) @@ -121,8 +121,8 @@ def test_updates_the_authorization_header_on_auth_events() -> None: def test_supports_setting_a_global_authorization_header() -> None: - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] authorization = "Bearer secretuserjwt" @@ -143,9 +143,9 @@ def test_supports_setting_a_global_authorization_header() -> None: assert client.storage.session.headers.get("Authorization") == authorization -def test_mutable_headers_issue(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +def test_mutable_headers_issue() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] shared_options = ClientOptions( storage=SyncMemoryStorage(), headers={"Authorization": "Bearer initial-token"} @@ -160,9 +160,9 @@ def test_mutable_headers_issue(): assert client1.options.headers["Authorization"] == "Bearer modified-token" -def test_global_authorization_header_issue(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +def test_global_authorization_header_issue() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] authorization = "Bearer secretuserjwt" options = ClientOptions(headers={"Authorization": authorization}) @@ -172,9 +172,9 @@ def test_global_authorization_header_issue(): assert client.options.headers.get("apiKey") == key -def test_httpx_client(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +def test_httpx_client() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] transport = HTTPTransport( retries=10, @@ -203,9 +203,9 @@ def test_httpx_client(): assert client.functions._client.timeout == Timeout(2.0) -def test_custom_headers(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +def test_custom_headers() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] options = ClientOptions( headers={ @@ -220,9 +220,9 @@ def test_custom_headers(): assert client.options.headers.get("x-version") == "1.0" -def test_custom_headers_immutable(): - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") +def test_custom_headers_immutable() -> None: + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] options = ClientOptions( headers={ @@ -241,14 +241,14 @@ def test_custom_headers_immutable(): assert client2.options.headers.get("x-app-name") == "apple" -def test_httpx_client_base_url_isolation(): +def test_httpx_client_base_url_isolation() -> None: """Test that shared httpx_client doesn't cause base_url mutation between services. This test reproduces the issue where accessing PostgREST after Storage causes Storage requests to hit the wrong endpoint (404 errors). See: https://github.com/supabase/supabase-py/issues/1244 """ - url = os.environ.get("SUPABASE_TEST_URL") - key = os.environ.get("SUPABASE_TEST_KEY") + url = os.environ["SUPABASE_TEST_URL"] + key = os.environ["SUPABASE_TEST_KEY"] # Create client with shared httpx instance timeout = Timeout(10.0, read=60.0) diff --git a/src/supabase/tests/test_client_options.py b/src/supabase/tests/test_client_options.py index a0453694..b4114458 100644 --- a/src/supabase/tests/test_client_options.py +++ b/src/supabase/tests/test_client_options.py @@ -1,19 +1,18 @@ -from supabase_auth import SyncMemoryStorage +from supabase_auth import AsyncMemoryStorage, SyncMemoryStorage from supabase import AClientOptions, ClientOptions class TestClientOptions: - def test_replace_returns_updated_aclient_options(self): - storage = SyncMemoryStorage() - storage.set_item("key", "value") + async def test_replace_returns_updated_aclient_options(self) -> None: + storage = AsyncMemoryStorage() + await storage.set_item("key", "value") options = AClientOptions( schema="schema", headers={"key": "value"}, auto_refresh_token=False, persist_session=False, storage=storage, - realtime={"key": "value"}, ) actual = options.replace(schema="new schema") @@ -23,12 +22,11 @@ def test_replace_returns_updated_aclient_options(self): auto_refresh_token=False, persist_session=False, storage=storage, - realtime={"key": "value"}, ) assert actual == expected - def test_replace_returns_updated_options(self): + def test_replace_returns_updated_options(self) -> None: storage = SyncMemoryStorage() storage.set_item("key", "value") options = ClientOptions( @@ -37,7 +35,6 @@ def test_replace_returns_updated_options(self): auto_refresh_token=False, persist_session=False, storage=storage, - realtime={"key": "value"}, ) actual = options.replace(schema="new schema") @@ -48,12 +45,11 @@ def test_replace_returns_updated_options(self): auto_refresh_token=False, persist_session=False, storage=storage, - realtime={"key": "value"}, ) assert actual == expected - def test_replace_updates_only_new_options(self): + def test_replace_updates_only_new_options(self) -> None: # Arrange storage = SyncMemoryStorage() storage.set_item("key", "value") diff --git a/src/supabase/tests/test_realtime.py b/src/supabase/tests/test_realtime.py index d4217ddd..74aef7cf 100644 --- a/src/supabase/tests/test_realtime.py +++ b/src/supabase/tests/test_realtime.py @@ -12,16 +12,3 @@ def test_realtime_client_initialization() -> None: url = "http://localhost:54322" sp_local = supabase.Client(url, key) assert sp_local.realtime_url == "ws://localhost:54322/realtime/v1" - - -def test_sync_realtime(): - ref = "ooqqmozurnggtljmjkii" - url = f"https://{ref}.supabase.co" - # Sample JWT Key - key = "xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx" - sp = supabase.Client(url, key) - - try: - sp.realtime.channel("test") - except NotImplementedError: - pass