Skip to content

Commit 5b5850f

Browse files
authored
Merge pull request #96 from joeriddles/add-client-options
Add typed client options
2 parents 2c7e530 + 6ce1cc0 commit 5b5850f

File tree

8 files changed

+133
-41
lines changed

8 files changed

+133
-41
lines changed

supabase/client.py

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
from typing import Any, Dict
1+
from typing import Any, Coroutine, Dict
22

3+
from httpx import Response
34
from postgrest_py import PostgrestClient
5+
from postgrest_py.request_builder import RequestBuilder
46

57
from supabase.lib.auth_client import SupabaseAuthClient
6-
from supabase.lib.constants import DEFAULT_HEADERS
8+
from supabase.lib.client_options import ClientOptions
9+
from supabase.lib.constants import DEFAULT_OPTIONS
710
from supabase.lib.query_builder import SupabaseQueryBuilder
811
from supabase.lib.realtime_client import SupabaseRealtimeClient
912
from supabase.lib.storage_client import SupabaseStorageClient
1013

11-
DEFAULT_OPTIONS = {
12-
"schema": "public",
13-
"auto_refresh_token": True,
14-
"persist_session": True,
15-
"detect_session_in_url": True,
16-
"local_storage": {},
17-
"headers": DEFAULT_HEADERS,
18-
}
19-
2014

2115
class Client:
2216
"""Supabase client class."""
@@ -47,19 +41,19 @@ def __init__(
4741
self.supabase_url = supabase_url
4842
self.supabase_key = supabase_key
4943

50-
settings = {**DEFAULT_OPTIONS, **options}
51-
settings["headers"].update(self._get_auth_headers())
44+
settings = DEFAULT_OPTIONS.replace(**options)
45+
settings.headers.update(self._get_auth_headers())
5246
self.rest_url: str = f"{supabase_url}/rest/v1"
5347
self.realtime_url: str = f"{supabase_url}/realtime/v1".replace("http", "ws")
5448
self.auth_url: str = f"{supabase_url}/auth/v1"
5549
self.storage_url = f"{supabase_url}/storage/v1"
56-
self.schema: str = settings.pop("schema")
50+
self.schema: str = settings.schema
5751

5852
# Instantiate clients.
5953
self.auth: SupabaseAuthClient = self._init_supabase_auth_client(
6054
auth_url=self.auth_url,
6155
supabase_key=self.supabase_key,
62-
**settings,
56+
client_options=settings,
6357
)
6458
# TODO(fedden): Bring up to parity with JS client.
6559
# self.realtime: SupabaseRealtimeClient = self._init_realtime_client(
@@ -70,14 +64,14 @@ def __init__(
7064
self.postgrest: PostgrestClient = self._init_postgrest_client(
7165
rest_url=self.rest_url,
7266
supabase_key=self.supabase_key,
73-
**settings,
67+
headers=settings.headers,
7468
)
7569

76-
def storage(self):
70+
def storage(self) -> SupabaseStorageClient:
7771
"""Create instance of the storage client"""
7872
return SupabaseStorageClient(self.storage_url, self._get_auth_headers())
7973

80-
def table(self, table_name: str):
74+
def table(self, table_name: str) -> RequestBuilder:
8175
"""Perform a table operation.
8276
8377
Note that the supabase client uses the `from` method, but in Python,
@@ -86,7 +80,7 @@ def table(self, table_name: str):
8680
"""
8781
return self.from_(table_name)
8882

89-
def from_(self, table_name: str):
83+
def from_(self, table_name: str) -> RequestBuilder:
9084
"""Perform a table operation.
9185
9286
See the `table` method.
@@ -100,7 +94,7 @@ def from_(self, table_name: str):
10094
)
10195
return query_builder.from_(table_name)
10296

103-
def rpc(self, fn, params):
97+
def rpc(self, fn: str, params: Dict[Any, Any]) -> Coroutine[Any, Any, Response]:
10498
"""Performs a stored procedure call.
10599
106100
Parameters
@@ -158,28 +152,23 @@ def _init_realtime_client(
158152
def _init_supabase_auth_client(
159153
auth_url: str,
160154
supabase_key: str,
161-
detect_session_in_url: bool,
162-
auto_refresh_token: bool,
163-
persist_session: bool,
164-
local_storage: Dict[str, Any],
165-
headers: Dict[str, str],
155+
client_options: ClientOptions,
166156
) -> SupabaseAuthClient:
167157
"""Creates a wrapped instance of the GoTrue Client."""
168158
return SupabaseAuthClient(
169159
url=auth_url,
170-
auto_refresh_token=auto_refresh_token,
171-
detect_session_in_url=detect_session_in_url,
172-
persist_session=persist_session,
173-
local_storage=local_storage,
174-
headers=headers,
160+
auto_refresh_token=client_options.auto_refresh_token,
161+
detect_session_in_url=client_options.detect_session_in_url,
162+
persist_session=client_options.persist_session,
163+
local_storage=client_options.local_storage,
164+
headers=client_options.headers,
175165
)
176166

177167
@staticmethod
178168
def _init_postgrest_client(
179169
rest_url: str,
180170
supabase_key: str,
181171
headers: Dict[str, str],
182-
**kwargs, # other unused settings
183172
) -> PostgrestClient:
184173
"""Private helper for creating an instance of the Postgrest client."""
185174
client = PostgrestClient(rest_url, headers=headers)
@@ -189,11 +178,10 @@ def _init_postgrest_client(
189178
def _get_auth_headers(self) -> Dict[str, str]:
190179
"""Helper method to get auth headers."""
191180
# What's the corresponding method to get the token
192-
headers: Dict[str, str] = {
181+
return {
193182
"apiKey": self.supabase_key,
194183
"Authorization": f"Bearer {self.supabase_key}",
195184
}
196-
return headers
197185

198186

199187
def create_client(supabase_url: str, supabase_key: str, **options) -> Client:

supabase/lib/client_options.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import copy
2+
import dataclasses
3+
from typing import Any, Callable, Dict, Optional
4+
5+
from supabase import __version__
6+
7+
DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"}
8+
9+
10+
@dataclasses.dataclass
11+
class ClientOptions:
12+
13+
"""The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."""
14+
15+
schema: str = "public"
16+
17+
"""Optional headers for initializing the client."""
18+
headers: Dict[str, str] = dataclasses.field(default_factory=DEFAULT_HEADERS.copy)
19+
20+
"""Automatically refreshes the token for logged in users."""
21+
auto_refresh_token: bool = True
22+
23+
"""Whether to persist a logged in session to storage."""
24+
persist_session: bool = True
25+
26+
"""Detect a session from the URL. Used for OAuth login callbacks."""
27+
detect_session_in_url: bool = True
28+
29+
"""A storage provider. Used to store the logged in session."""
30+
local_storage: Dict[str, Any] = dataclasses.field(default_factory=lambda: {})
31+
32+
"""Options passed to the realtime-py instance"""
33+
realtime: Optional[Dict[str, Any]] = None
34+
35+
"""A custom `fetch` implementation."""
36+
fetch: Optional[Callable] = None
37+
38+
def replace(
39+
self,
40+
schema: Optional[str] = None,
41+
headers: Optional[Dict[str, str]] = None,
42+
auto_refresh_token: Optional[bool] = None,
43+
persist_session: Optional[bool] = None,
44+
detect_session_in_url: Optional[bool] = None,
45+
local_storage: Optional[Dict[str, Any]] = None,
46+
realtime: Optional[Dict[str, Any]] = None,
47+
fetch: Optional[Callable] = None,
48+
) -> "ClientOptions":
49+
"""Create a new SupabaseClientOptions with changes"""
50+
changes = {
51+
key: value
52+
for key, value in locals().items()
53+
if key != "self" and value is not None
54+
}
55+
client_options = dataclasses.replace(self, **changes)
56+
client_options = copy.deepcopy(client_options)
57+
return client_options

supabase/lib/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from supabase import __version__
1+
from supabase.lib.client_options import ClientOptions
22

3-
DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"}
3+
DEFAULT_OPTIONS: ClientOptions = ClientOptions()

supabase/lib/storage_client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Dict
2+
13
from supabase.lib.storage.storage_bucket_api import StorageBucketAPI
24
from supabase.lib.storage.storage_file_api import StorageFileAPI
35

@@ -14,8 +16,8 @@ class SupabaseStorageClient(StorageBucketAPI):
1416
>>> list_files = storage_file.list("something")
1517
"""
1618

17-
def __init__(self, url, headers):
19+
def __init__(self, url: str, headers: Dict[str, str]):
1820
super().__init__(url, headers)
1921

20-
def StorageFileAPI(self, id_):
22+
def StorageFileAPI(self, id_: str) -> StorageFileAPI:
2123
return StorageFileAPI(self.url, self.headers, id_)

supabase/py.typed

Whitespace-only changes.

test.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
powershell -Command {
2+
$env:SUPABASE_TEST_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNTAwODQ4NywiZXhwIjoxOTUwNTg0NDg3fQ.l8IgkO7TQokGSc9OJoobXIVXsOXkilXl4Ak6SCX5qI8";
3+
$env:SUPABASE_TEST_URL = "https://ibrydvrsxoapzgtnhpso.supabase.co";
4+
poetry run pytest;
5+
}

tests/conftest.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
@pytest.fixture(scope="session")
1111
def supabase() -> Client:
12-
url: str = os.environ.get("SUPABASE_TEST_URL")
13-
key: str = os.environ.get("SUPABASE_TEST_KEY")
14-
supabase: Client = create_client(url, key)
15-
return supabase
12+
url = os.environ.get("SUPABASE_TEST_URL")
13+
assert url is not None, "Must provide SUPABASE_TEST_URL environment variable"
14+
key = os.environ.get("SUPABASE_TEST_KEY")
15+
assert key is not None, "Must provide SUPABASE_TEST_KEY environment variable"
16+
return create_client(url, key)

tests/test_client_options.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from supabase.lib.client_options import ClientOptions
2+
3+
4+
def test__client_options__replace__returns_updated_options():
5+
options = ClientOptions(
6+
schema="schema",
7+
headers={"key": "value"},
8+
auto_refresh_token=False,
9+
persist_session=False,
10+
detect_session_in_url=False,
11+
local_storage={"key": "value"},
12+
realtime={"key": "value"},
13+
)
14+
15+
actual = options.replace(schema="new schema")
16+
expected = ClientOptions(
17+
schema="new schema",
18+
headers={"key": "value"},
19+
auto_refresh_token=False,
20+
persist_session=False,
21+
detect_session_in_url=False,
22+
local_storage={"key": "value"},
23+
realtime={"key": "value"},
24+
)
25+
26+
assert actual == expected
27+
28+
29+
def test__client_options__replace__updates_only_new_options():
30+
# Arrange
31+
options = ClientOptions(local_storage={"key": "value"})
32+
new_options = options.replace()
33+
34+
# Act
35+
new_options.local_storage["key"] = "new_value"
36+
37+
# Assert
38+
assert options.local_storage["key"] == "value"
39+
assert new_options.local_storage["key"] == "new_value"

0 commit comments

Comments
 (0)