Skip to content

Commit ee64181

Browse files
author
Andrew Smith
authored
feat: add async client (#619)
2 parents f0dbe94 + c34d5c6 commit ee64181

File tree

13 files changed

+883
-504
lines changed

13 files changed

+883
-504
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ run_tests: tests
1414

1515
tests_only:
1616
poetry run pytest --cov=./ --cov-report=xml --cov-report=html -vv
17+
18+
build_sync:
19+
poetry run unasync supabase tests

poetry.lock

Lines changed: 230 additions & 195 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ python-dotenv = "^1.0.0"
3737
[tool.poetry.scripts]
3838
tests = 'poetry_scripts:run_tests'
3939

40+
[tool.poetry.group.dev.dependencies]
41+
unasync-cli = "^0.0.9"
42+
4043
[tool.semantic_release]
4144
version_variables = ["supabase/__version__.py:__version__"]
4245
version_toml = ["pyproject.toml:tool.poetry.version"]

supabase/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from storage3.utils import StorageException
44

55
from .__version__ import __version__
6-
from .client import Client, create_client
7-
from .lib.auth_client import SupabaseAuthClient
6+
from ._sync.auth_client import SyncSupabaseAuthClient as SupabaseAuthClient
7+
from ._sync.client import Client
8+
from ._sync.client import SyncStorageClient as SupabaseStorageClient
9+
from ._sync.client import create_client
810
from .lib.realtime_client import SupabaseRealtimeClient
9-
from .lib.storage_client import SupabaseStorageClient

supabase/_async/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from __future__ import annotations

supabase/_async/auth_client.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Dict, Optional
2+
3+
from gotrue import (
4+
AsyncGoTrueClient,
5+
AsyncMemoryStorage,
6+
AsyncSupportedStorage,
7+
AuthFlowType,
8+
)
9+
from gotrue.http_clients import AsyncClient
10+
11+
12+
class AsyncSupabaseAuthClient(AsyncGoTrueClient):
13+
"""SupabaseAuthClient"""
14+
15+
def __init__(
16+
self,
17+
*,
18+
url: str,
19+
headers: Optional[Dict[str, str]] = None,
20+
storage_key: Optional[str] = None,
21+
auto_refresh_token: bool = True,
22+
persist_session: bool = True,
23+
storage: AsyncSupportedStorage = AsyncMemoryStorage(),
24+
http_client: Optional[AsyncClient] = None,
25+
flow_type: AuthFlowType = "implicit"
26+
):
27+
"""Instantiate SupabaseAuthClient instance."""
28+
if headers is None:
29+
headers = {}
30+
31+
AsyncGoTrueClient.__init__(
32+
self,
33+
url=url,
34+
headers=headers,
35+
storage_key=storage_key,
36+
auto_refresh_token=auto_refresh_token,
37+
persist_session=persist_session,
38+
storage=storage,
39+
http_client=http_client,
40+
flow_type=flow_type,
41+
)

supabase/_async/client.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import re
2+
from typing import Any, Dict, Union
3+
4+
from deprecation import deprecated
5+
from gotrue.types import AuthChangeEvent
6+
from httpx import Timeout
7+
from postgrest import (
8+
AsyncFilterRequestBuilder,
9+
AsyncPostgrestClient,
10+
AsyncRequestBuilder,
11+
)
12+
from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT
13+
from storage3 import AsyncStorageClient
14+
from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT
15+
from supafunc import AsyncFunctionsClient
16+
17+
from ..lib.client_options import ClientOptions
18+
from .auth_client import AsyncSupabaseAuthClient
19+
20+
21+
# Create an exception class when user does not provide a valid url or key.
22+
class SupabaseException(Exception):
23+
def __init__(self, message: str):
24+
self.message = message
25+
super().__init__(self.message)
26+
27+
28+
class Client:
29+
"""Supabase client class."""
30+
31+
def __init__(
32+
self,
33+
supabase_url: str,
34+
supabase_key: str,
35+
options: ClientOptions = ClientOptions(),
36+
):
37+
"""Instantiate the client.
38+
39+
Parameters
40+
----------
41+
supabase_url: str
42+
The URL to the Supabase instance that should be connected to.
43+
supabase_key: str
44+
The API key to the Supabase instance that should be connected to.
45+
**options
46+
Any extra settings to be optionally specified - also see the
47+
`DEFAULT_OPTIONS` dict.
48+
"""
49+
50+
if not supabase_url:
51+
raise SupabaseException("supabase_url is required")
52+
if not supabase_key:
53+
raise SupabaseException("supabase_key is required")
54+
55+
# Check if the url and key are valid
56+
if not re.match(r"^(https?)://.+", supabase_url):
57+
raise SupabaseException("Invalid URL")
58+
59+
# Check if the key is a valid JWT
60+
if not re.match(
61+
r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$", supabase_key
62+
):
63+
raise SupabaseException("Invalid API key")
64+
65+
self.supabase_url = supabase_url
66+
self.supabase_key = supabase_key
67+
options.headers.update(self._get_auth_headers())
68+
self.options = options
69+
self.rest_url = f"{supabase_url}/rest/v1"
70+
self.realtime_url = f"{supabase_url}/realtime/v1".replace("http", "ws")
71+
self.auth_url = f"{supabase_url}/auth/v1"
72+
self.storage_url = f"{supabase_url}/storage/v1"
73+
self.functions_url = f"{supabase_url}/functions/v1"
74+
self.schema = options.schema
75+
76+
# Instantiate clients.
77+
self.auth = self._init_supabase_auth_client(
78+
auth_url=self.auth_url,
79+
client_options=options,
80+
)
81+
# TODO: Bring up to parity with JS client.
82+
# self.realtime: SupabaseRealtimeClient = self._init_realtime_client(
83+
# realtime_url=self.realtime_url,
84+
# supabase_key=self.supabase_key,
85+
# )
86+
self.realtime = None
87+
self._postgrest = None
88+
self._storage = None
89+
self._functions = None
90+
self.auth.on_auth_state_change(self._listen_to_auth_events)
91+
92+
@deprecated("1.1.1", "1.3.0", details="Use `.functions` instead")
93+
def functions(self) -> AsyncFunctionsClient:
94+
return AsyncFunctionsClient(self.functions_url, self._get_auth_headers())
95+
96+
def table(self, table_name: str) -> AsyncRequestBuilder:
97+
"""Perform a table operation.
98+
99+
Note that the supabase client uses the `from` method, but in Python,
100+
this is a reserved keyword, so we have elected to use the name `table`.
101+
Alternatively you can use the `.from_()` method.
102+
"""
103+
return self.from_(table_name)
104+
105+
def from_(self, table_name: str) -> AsyncRequestBuilder:
106+
"""Perform a table operation.
107+
108+
See the `table` method.
109+
"""
110+
return self.postgrest.from_(table_name)
111+
112+
def rpc(self, fn: str, params: Dict[Any, Any]) -> AsyncFilterRequestBuilder:
113+
"""Performs a stored procedure call.
114+
115+
Parameters
116+
----------
117+
fn : callable
118+
The stored procedure call to be executed.
119+
params : dict of any
120+
Parameters passed into the stored procedure call.
121+
122+
Returns
123+
-------
124+
SyncFilterRequestBuilder
125+
Returns a filter builder. This lets you apply filters on the response
126+
of an RPC.
127+
"""
128+
return self.postgrest.rpc(fn, params)
129+
130+
@property
131+
def postgrest(self):
132+
if self._postgrest is None:
133+
self.options.headers.update(self._get_token_header())
134+
self._postgrest = self._init_postgrest_client(
135+
rest_url=self.rest_url,
136+
headers=self.options.headers,
137+
schema=self.options.schema,
138+
timeout=self.options.postgrest_client_timeout,
139+
)
140+
return self._postgrest
141+
142+
@property
143+
def storage(self):
144+
if self._storage is None:
145+
headers = self._get_auth_headers()
146+
headers.update(self._get_token_header())
147+
self._storage = self._init_storage_client(
148+
storage_url=self.storage_url,
149+
headers=headers,
150+
storage_client_timeout=self.options.storage_client_timeout,
151+
)
152+
return self._storage
153+
154+
@property
155+
def functions(self):
156+
if self._functions is None:
157+
headers = self._get_auth_headers()
158+
headers.update(self._get_token_header())
159+
self._functions = AsyncFunctionsClient(self.functions_url, headers)
160+
return self._functions
161+
162+
# async def remove_subscription_helper(resolve):
163+
# try:
164+
# await self._close_subscription(subscription)
165+
# open_subscriptions = len(self.get_subscriptions())
166+
# if not open_subscriptions:
167+
# error = await self.realtime.disconnect()
168+
# if error:
169+
# return {"error": None, "data": { open_subscriptions}}
170+
# except Exception as e:
171+
# raise e
172+
# return remove_subscription_helper(subscription)
173+
174+
# async def _close_subscription(self, subscription):
175+
# """Close a given subscription
176+
177+
# Parameters
178+
# ----------
179+
# subscription
180+
# The name of the channel
181+
# """
182+
# if not subscription.closed:
183+
# await self._closeChannel(subscription)
184+
185+
# def get_subscriptions(self):
186+
# """Return all channels the client is subscribed to."""
187+
# return self.realtime.channels
188+
189+
# @staticmethod
190+
# def _init_realtime_client(
191+
# realtime_url: str, supabase_key: str
192+
# ) -> SupabaseRealtimeClient:
193+
# """Private method for creating an instance of the realtime-py client."""
194+
# return SupabaseRealtimeClient(
195+
# realtime_url, {"params": {"apikey": supabase_key}}
196+
# )
197+
@staticmethod
198+
def _init_storage_client(
199+
storage_url: str,
200+
headers: Dict[str, str],
201+
storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT,
202+
) -> AsyncStorageClient:
203+
return AsyncStorageClient(storage_url, headers, storage_client_timeout)
204+
205+
@staticmethod
206+
def _init_supabase_auth_client(
207+
auth_url: str,
208+
client_options: ClientOptions,
209+
) -> AsyncSupabaseAuthClient:
210+
"""Creates a wrapped instance of the GoTrue Client."""
211+
return AsyncSupabaseAuthClient(
212+
url=auth_url,
213+
auto_refresh_token=client_options.auto_refresh_token,
214+
persist_session=client_options.persist_session,
215+
storage=client_options.storage,
216+
headers=client_options.headers,
217+
flow_type=client_options.flow_type,
218+
)
219+
220+
@staticmethod
221+
def _init_postgrest_client(
222+
rest_url: str,
223+
headers: Dict[str, str],
224+
schema: str,
225+
timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT,
226+
) -> AsyncPostgrestClient:
227+
"""Private helper for creating an instance of the Postgrest client."""
228+
return AsyncPostgrestClient(
229+
rest_url, headers=headers, schema=schema, timeout=timeout
230+
)
231+
232+
def _get_auth_headers(self) -> Dict[str, str]:
233+
"""Helper method to get auth headers."""
234+
return {
235+
"apiKey": self.supabase_key,
236+
"Authorization": f"Bearer {self.supabase_key}",
237+
}
238+
239+
def _get_token_header(self):
240+
try:
241+
access_token = self.auth.get_session().access_token
242+
except:
243+
access_token = self.supabase_key
244+
245+
return {
246+
"Authorization": f"Bearer {access_token}",
247+
}
248+
249+
def _listen_to_auth_events(self, event: AuthChangeEvent, session):
250+
if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]:
251+
# reset postgrest and storage instance on event change
252+
self._postgrest = None
253+
self._storage = None
254+
self._functions = None
255+
256+
257+
def create_client(
258+
supabase_url: str,
259+
supabase_key: str,
260+
options: ClientOptions = ClientOptions(),
261+
) -> Client:
262+
"""Create client function to instantiate supabase client like JS runtime.
263+
264+
Parameters
265+
----------
266+
supabase_url: str
267+
The URL to the Supabase instance that should be connected to.
268+
supabase_key: str
269+
The API key to the Supabase instance that should be connected to.
270+
**options
271+
Any extra settings to be optionally specified - also see the
272+
`DEFAULT_OPTIONS` dict.
273+
274+
Examples
275+
--------
276+
Instantiating the client.
277+
>>> import os
278+
>>> from supabase import create_client, Client
279+
>>>
280+
>>> url: str = os.environ.get("SUPABASE_TEST_URL")
281+
>>> key: str = os.environ.get("SUPABASE_TEST_KEY")
282+
>>> supabase: Client = create_client(url, key)
283+
284+
Returns
285+
-------
286+
Client
287+
"""
288+
return Client(supabase_url=supabase_url, supabase_key=supabase_key, options=options)

supabase/_sync/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from __future__ import annotations

supabase/lib/auth_client.py renamed to supabase/_sync/auth_client.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,10 @@
66
SyncMemoryStorage,
77
SyncSupportedStorage,
88
)
9+
from gotrue.http_clients import SyncClient
910

10-
# TODO - export this from GoTrue-py in next release
11-
from httpx import Client as BaseClient
1211

13-
14-
class SyncClient(BaseClient):
15-
def aclose(self) -> None:
16-
self.close()
17-
18-
19-
class SupabaseAuthClient(SyncGoTrueClient):
12+
class SyncSupabaseAuthClient(SyncGoTrueClient):
2013
"""SupabaseAuthClient"""
2114

2215
def __init__(

0 commit comments

Comments
 (0)