Skip to content

Commit

Permalink
Add typed client options
Browse files Browse the repository at this point in the history
  • Loading branch information
joeriddles committed Dec 15, 2021
1 parent 55e8f84 commit b228d2b
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 38 deletions.
52 changes: 20 additions & 32 deletions supabase/client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
from typing import Any, Dict
from typing import Any, Coroutine, Dict

from httpx import Response
from postgrest_py import PostgrestClient
from postgrest_py.request_builder import RequestBuilder

from supabase.lib.auth_client import SupabaseAuthClient
from supabase.lib.constants import DEFAULT_HEADERS
from supabase.lib.client_options import ClientOptions
from supabase.lib.constants import DEFAULT_OPTIONS
from supabase.lib.query_builder import SupabaseQueryBuilder
from supabase.lib.realtime_client import SupabaseRealtimeClient
from supabase.lib.storage_client import SupabaseStorageClient

DEFAULT_OPTIONS = {
"schema": "public",
"auto_refresh_token": True,
"persist_session": True,
"detect_session_in_url": True,
"local_storage": {},
"headers": DEFAULT_HEADERS,
}


class Client:
"""Supabase client class."""
Expand Down Expand Up @@ -47,19 +40,19 @@ def __init__(
self.supabase_url = supabase_url
self.supabase_key = supabase_key

settings = {**DEFAULT_OPTIONS, **options}
settings["headers"].update(self._get_auth_headers())
settings = DEFAULT_OPTIONS.replace(**options)
settings.headers.update(self._get_auth_headers())
self.rest_url: str = f"{supabase_url}/rest/v1"
self.realtime_url: str = f"{supabase_url}/realtime/v1".replace("http", "ws")
self.auth_url: str = f"{supabase_url}/auth/v1"
self.storage_url = f"{supabase_url}/storage/v1"
self.schema: str = settings.pop("schema")
self.schema: str = settings.schema

# Instantiate clients.
self.auth: SupabaseAuthClient = self._init_supabase_auth_client(
auth_url=self.auth_url,
supabase_key=self.supabase_key,
**settings,
client_options=settings,
)
# TODO(fedden): Bring up to parity with JS client.
# self.realtime: SupabaseRealtimeClient = self._init_realtime_client(
Expand All @@ -70,14 +63,14 @@ def __init__(
self.postgrest: PostgrestClient = self._init_postgrest_client(
rest_url=self.rest_url,
supabase_key=self.supabase_key,
**settings,
headers=settings.headers,
)

def storage(self):
def storage(self) -> SupabaseStorageClient:
"""Create instance of the storage client"""
return SupabaseStorageClient(self.storage_url, self._get_auth_headers())

def table(self, table_name: str):
def table(self, table_name: str) -> RequestBuilder:
"""Perform a table operation.
Note that the supabase client uses the `from` method, but in Python,
Expand All @@ -86,7 +79,7 @@ def table(self, table_name: str):
"""
return self.from_(table_name)

def from_(self, table_name: str):
def from_(self, table_name: str) -> RequestBuilder:
"""Perform a table operation.
See the `table` method.
Expand All @@ -100,7 +93,7 @@ def from_(self, table_name: str):
)
return query_builder.from_(table_name)

def rpc(self, fn, params):
def rpc(self, fn, params) -> Coroutine[Any, Any, Response]:
"""Performs a stored procedure call.
Parameters
Expand Down Expand Up @@ -158,28 +151,23 @@ def _init_realtime_client(
def _init_supabase_auth_client(
auth_url: str,
supabase_key: str,
detect_session_in_url: bool,
auto_refresh_token: bool,
persist_session: bool,
local_storage: Dict[str, Any],
headers: Dict[str, str],
client_options: ClientOptions,
) -> SupabaseAuthClient:
"""Creates a wrapped instance of the GoTrue Client."""
return SupabaseAuthClient(
url=auth_url,
auto_refresh_token=auto_refresh_token,
detect_session_in_url=detect_session_in_url,
persist_session=persist_session,
local_storage=local_storage,
headers=headers,
auto_refresh_token=client_options.auto_refresh_token,
detect_session_in_url=client_options.detect_session_in_url,
persist_session=client_options.persist_session,
local_storage=client_options.local_storage,
headers=client_options.headers,
)

@staticmethod
def _init_postgrest_client(
rest_url: str,
supabase_key: str,
headers: Dict[str, str],
**kwargs, # other unused settings
) -> PostgrestClient:
"""Private helper for creating an instance of the Postgrest client."""
client = PostgrestClient(rest_url, headers=headers)
Expand Down
58 changes: 58 additions & 0 deletions supabase/lib/client_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import copy
import dataclasses
from typing import Any, Callable, Dict, Optional

from supabase import __version__


DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"}


@dataclasses.dataclass
class ClientOptions:

"""The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to 'public'."""

schema: str = "public"

"""Optional headers for initializing the client."""
headers: Dict[str, str] = dataclasses.field(default_factory=DEFAULT_HEADERS.copy)

"""Automatically refreshes the token for logged in users."""
auto_refresh_token: bool = True

"""Whether to persist a logged in session to storage."""
persist_session: bool = True

"""Detect a session from the URL. Used for OAuth login callbacks."""
detect_session_in_url: bool = True

"""A storage provider. Used to store the logged in session."""
local_storage: Dict[str, Any] = dataclasses.field(default_factory=lambda: {})

"""Options passed to the realtime-js instance"""
realtime: Optional[Dict[str, Any]] = None

"""A custom `fetch` implementation."""
fetch: Optional[Callable] = None

def replace(
self,
schema: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
auto_refresh_token: Optional[bool] = None,
persist_session: Optional[bool] = None,
detect_session_in_url: Optional[bool] = None,
local_storage: Optional[Dict[str, Any]] = None,
realtime: Optional[Dict[str, Any]] = None,
fetch: Optional[Callable] = None,
) -> "ClientOptions":
"""Create a new SupabaseClientOptions with changes"""
changes = {
key: value
for key, value in locals().items()
if key != "self" and value is not None
}
client_options = dataclasses.replace(self, **changes)
client_options = copy.deepcopy(client_options)
return client_options
5 changes: 3 additions & 2 deletions supabase/lib/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from supabase import __version__
from supabase.lib.client_options import ClientOptions

DEFAULT_HEADERS = {"X-Client-Info": f"supabase-py/{__version__}"}

DEFAULT_OPTIONS: ClientOptions = ClientOptions()
6 changes: 4 additions & 2 deletions supabase/lib/storage_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Dict

from supabase.lib.storage.storage_bucket_api import StorageBucketAPI
from supabase.lib.storage.storage_file_api import StorageFileAPI

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

def __init__(self, url, headers):
def __init__(self, url: str, headers: Dict[str, str]):
super().__init__(url, headers)

def StorageFileAPI(self, id_):
def StorageFileAPI(self, id_: str):
return StorageFileAPI(self.url, self.headers, id_)
5 changes: 5 additions & 0 deletions test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
powershell -Command {
$env:SUPABASE_TEST_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNTAwODQ4NywiZXhwIjoxOTUwNTg0NDg3fQ.l8IgkO7TQokGSc9OJoobXIVXsOXkilXl4Ak6SCX5qI8";
$env:SUPABASE_TEST_URL = "https://ibrydvrsxoapzgtnhpso.supabase.co";
poetry run pytest;
}
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

@pytest.fixture(scope="session")
def supabase() -> Client:
url: str = os.environ.get("SUPABASE_TEST_URL")
key: str = os.environ.get("SUPABASE_TEST_KEY")
url = os.environ.get("SUPABASE_TEST_URL")
assert url is not None, "Must provide SUPABASE_TEST_URL environment variable"
key = os.environ.get("SUPABASE_TEST_KEY")
assert key is not None, "Must provide SUPABASE_TEST_KEY environment variable"
supabase: Client = create_client(url, key)
return supabase
39 changes: 39 additions & 0 deletions tests/test_client_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from supabase.lib.client_options import ClientOptions


def test__client_options__replace__returns_updated_options():
options = ClientOptions(
schema="schema",
headers={"key": "value"},
auto_refresh_token=False,
persist_session=False,
detect_session_in_url=False,
local_storage={"key": "value"},
realtime={"key": "value"}
)

actual = options.replace(schema="new schema")
expected = ClientOptions(
schema="new schema",
headers={"key": "value"},
auto_refresh_token=False,
persist_session=False,
detect_session_in_url=False,
local_storage={"key": "value"},
realtime={"key": "value"}
)

assert actual == expected


def test__client_options__replace__updates_only_new_options():
# Arrange
options = ClientOptions(local_storage={"key": "value"})
new_options = options.replace()

# Act
new_options.local_storage["key"] = "new_value"

# Assert
assert options.local_storage["key"] == "value"
assert new_options.local_storage["key"] == "new_value"

0 comments on commit b228d2b

Please sign in to comment.