diff --git a/poetry.lock b/poetry.lock index 8a9483d6..1b8d683d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1729,18 +1729,19 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "58.5.3" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "setuptools-58.5.3-py3-none-any.whl", hash = "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf"}, - {file = "setuptools-58.5.3.tar.gz", hash = "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-inline-tabs", "sphinxcontrib-towncrier"] -testing = ["flake8-2020", "jaraco.envs", "jaraco.path (>=3.2.0)", "mock", "paver", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-virtualenv (>=1.2.7)", "pytest-xdist", "sphinx", "virtualenv (>=13.0.0)", "wheel"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" @@ -1846,6 +1847,17 @@ files = [ [package.extras] tests = ["pytest", "pytest-cov"] +[[package]] +name = "tokenize-rt" +version = "6.0.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tokenize_rt-6.0.0-py2.py3-none-any.whl", hash = "sha256:d4ff7ded2873512938b4f8cbb98c9b07118f01d30ac585a30d7a88353ca36d22"}, + {file = "tokenize_rt-6.0.0.tar.gz", hash = "sha256:b9711bdfc51210211137499b5e355d3de5ec88a85d2025c520cbb921b5194367"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -1901,30 +1913,38 @@ files = [ [[package]] name = "unasync" -version = "0.5.0" +version = "0.6.0" description = "The async transformation code." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.8" files = [ - {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, - {file = "unasync-0.5.0.tar.gz", hash = "sha256:b675d87cf56da68bd065d3b7a67ac71df85591978d84c53083c20d79a7e5096d"}, + {file = "unasync-0.6.0-py3-none-any.whl", hash = "sha256:9cf7aaaea9737e417d8949bf9be55dc25fdb4ef1f4edc21b58f76ff0d2b9d73f"}, + {file = "unasync-0.6.0.tar.gz", hash = "sha256:a9d01ace3e1068b20550ab15b7f9723b15b8bcde728bc1770bcb578374c7ee58"}, ] +[package.dependencies] +setuptools = "*" +tokenize-rt = "*" + [[package]] name = "unasync-cli" -version = "0.0.9" -description = "Command line interface for unasync" +version = "0.0.1" +description = "Command line interface for unasync. Fork of https://github.com/leynier/unasync-cli/" optional = false -python-versions = ">=3.6.14,<4.0.0" -files = [ - {file = "unasync-cli-0.0.9.tar.gz", hash = "sha256:ca9d8c57ebb68911f8f8f68f243c7f6d0bb246ee3fd14743bc51c8317e276554"}, - {file = "unasync_cli-0.0.9-py3-none-any.whl", hash = "sha256:f96c42fb2862efa555ce6d6415a5983ceb162aa0e45be701656d20a955c7c540"}, -] +python-versions = "^3.8.18" +files = [] +develop = false [package.dependencies] -setuptools = ">=58.2.0,<59.0.0" -typer = ">=0.4.0,<0.5.0" -unasync = ">=0.5.0,<0.6.0" +setuptools = "^72.1.0" +typer = "^0.4.0" +unasync = "^0.6.0" + +[package.source] +type = "git" +url = "https://github.com/supabase-community/unasync-cli.git" +reference = "main" +resolved_reference = "22b8b0e86608ae8adff3623cb0bbc7d378afe266" [[package]] name = "urllib3" @@ -2175,5 +2195,5 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "c578f90f27af2382535bdc134afa64896b51b4954ba6488d30f5fbce3d5c67ea" +python-versions = "^3.8.18" +content-hash = "c3ab59513f00db183b0cfd23e757e1617b09f07235f477ca04ab987a575de68a" diff --git a/pyproject.toml b/pyproject.toml index 0ae84191..bf3ff0d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.8.18" postgrest = ">=0.14,<0.17.0" realtime = { path = "../realtime-py", develop = true } gotrue = ">=1.3,<3.0" @@ -38,7 +38,7 @@ python-dotenv = "^1.0.1" tests = 'poetry_scripts:run_tests' [tool.poetry.group.dev.dependencies] -unasync-cli = "^0.0.9" +unasync-cli = { git = "https://github.com/supabase-community/unasync-cli.git", branch = "main" } [tool.semantic_release] version_variables = ["supabase/__version__.py:__version__"] diff --git a/supabase/__init__.py b/supabase/__init__.py index 28ad49f1..15e35f55 100644 --- a/supabase/__init__.py +++ b/supabase/__init__.py @@ -19,9 +19,6 @@ from ._sync.client import SyncStorageClient as SupabaseStorageClient from ._sync.client import create_client -# Realtime Client -from .lib.realtime_client import SupabaseRealtimeClient - __all__ = [ "acreate_client", "AClient", @@ -31,7 +28,6 @@ "Client", "SupabaseAuthClient", "SupabaseStorageClient", - "SupabaseRealtimeClient", "PostgrestAPIError", "PostgrestAPIResponse", "StorageException", diff --git a/supabase/_sync/client.py b/supabase/_sync/client.py index 527b030c..9d1cbc46 100644 --- a/supabase/_sync/client.py +++ b/supabase/_sync/client.py @@ -1,5 +1,5 @@ import re -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Union from gotrue import SyncMemoryStorage from gotrue.types import AuthChangeEvent, Session @@ -10,6 +10,8 @@ SyncRPCFilterRequestBuilder, ) from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT +from realtime.channel import Channel, RealtimeChannelOptions +from realtime.client import RealtimeClient from storage3 import SyncStorageClient from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT from supafunc import SyncFunctionsClient @@ -80,11 +82,11 @@ def __init__( auth_url=self.auth_url, client_options=options, ) - # TODO: Bring up to parity with JS client. - # self.realtime: SupabaseRealtimeClient = self._init_realtime_client( - # realtime_url=self.realtime_url, - # supabase_key=self.supabase_key, - # ) + self.realtime: RealtimeClient = self._init_realtime_client( + realtime_url=self.realtime_url, + supabase_key=self.supabase_key, + options=options.realtime if options else None, + ) self.realtime = None self._postgrest = None self._storage = None @@ -197,41 +199,29 @@ def functions(self): ) return self._functions - # async def remove_subscription_helper(resolve): - # try: - # await self._close_subscription(subscription) - # open_subscriptions = len(self.get_subscriptions()) - # if not open_subscriptions: - # error = await self.realtime.disconnect() - # if error: - # return {"error": None, "data": { open_subscriptions}} - # except Exception as e: - # raise e - # return remove_subscription_helper(subscription) - - # async def _close_subscription(self, subscription): - # """Close a given subscription - - # Parameters - # ---------- - # subscription - # The name of the channel - # """ - # if not subscription.closed: - # await self._closeChannel(subscription) - - # def get_subscriptions(self): - # """Return all channels the client is subscribed to.""" - # return self.realtime.channels - - # @staticmethod - # def _init_realtime_client( - # realtime_url: str, supabase_key: str - # ) -> SupabaseRealtimeClient: - # """Private method for creating an instance of the realtime-py client.""" - # return SupabaseRealtimeClient( - # realtime_url, {"params": {"apikey": supabase_key}} - # ) + def channel(self, topic: str, params: RealtimeChannelOptions = {}) -> Channel: + """Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.""" + return self.realtime.channel(topic, params) + + def get_channels(self) -> List[Channel]: + """Returns all realtime channels.""" + return self.realtime.get_channels() + + def remove_channel(self, channel: Channel) -> None: + """Unsubscribes and removes Realtime channel from Realtime client.""" + self.realtime.remove_channel(channel) + + 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]] + ) -> RealtimeClient: + """Private method for creating an instance of the realtime-py client.""" + return RealtimeClient(realtime_url, token=supabase_key, params=options or {}) + @staticmethod def _init_storage_client( storage_url: str, @@ -303,6 +293,9 @@ def _listen_to_auth_events( self.options.headers["Authorization"] = self._create_auth_header(access_token) + # set_auth is a coroutine, how to handle this? + self.realtime.set_auth(access_token) + def create_client( supabase_url: str, diff --git a/tests/test_client.py b/tests/test_client.py index e620179c..d5320b8e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -72,6 +72,9 @@ def test_updates_the_authorization_header_on_auth_events() -> None: assert client.options.headers.get("Authorization") == f"Bearer {key}" mock_session = MagicMock(access_token="secretuserjwt") + realtime_mock = MagicMock() + client.realtime = realtime_mock + client._listen_to_auth_events("SIGNED_IN", mock_session) updated_authorization = f"Bearer {mock_session.access_token}" @@ -89,3 +92,5 @@ def test_updates_the_authorization_header_on_auth_events() -> None: assert client.storage.session.headers.get("apiKey") == key assert client.storage.session.headers.get("Authorization") == updated_authorization + + realtime_mock.set_auth.assert_called_once_with(mock_session.access_token) diff --git a/tests/test_realtime_configuration.py b/tests/test_realtime_configuration.py new file mode 100644 index 00000000..62e3050a --- /dev/null +++ b/tests/test_realtime_configuration.py @@ -0,0 +1,14 @@ +import supabase + + +def test_functions_client_initialization() -> None: + ref = "ooqqmozurnggtljmjkii" + url = f"https://{ref}.supabase.co" + # Sample JWT Key + key = "xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx" + sp = supabase.Client(url, key) + assert sp.realtime_url == f"wss://{ref}.supabase.co/realtime/v1" + + url = "http://localhost:54322" + sp_local = supabase.Client(url, key) + assert sp_local.realtime_url == f"ws://localhost:54322/realtime/v1"