Skip to content

Add typed client options #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 21 additions & 33 deletions supabase/client.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
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 +41,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 +64,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 +80,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 +94,7 @@ def from_(self, table_name: str):
)
return query_builder.from_(table_name)

def rpc(self, fn, params):
def rpc(self, fn: str, params: Dict[Any, Any]) -> Coroutine[Any, Any, Response]:
"""Performs a stored procedure call.

Parameters
Expand Down Expand Up @@ -158,28 +152,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 All @@ -189,11 +178,10 @@ def _init_postgrest_client(
def _get_auth_headers(self) -> Dict[str, str]:
"""Helper method to get auth headers."""
# What's the corresponding method to get the token
headers: Dict[str, str] = {
return {
"apiKey": self.supabase_key,
"Authorization": f"Bearer {self.supabase_key}",
}
return headers


def create_client(supabase_url: str, supabase_key: str, **options) -> Client:
Expand Down
57 changes: 57 additions & 0 deletions supabase/lib/client_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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-py 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
4 changes: 2 additions & 2 deletions supabase/lib/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
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) -> StorageFileAPI:
return StorageFileAPI(self.url, self.headers, id_)
Empty file added supabase/py.typed
Empty file.
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;
}
9 changes: 5 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

@pytest.fixture(scope="session")
def supabase() -> Client:
url: str = os.environ.get("SUPABASE_TEST_URL")
key: str = os.environ.get("SUPABASE_TEST_KEY")
supabase: Client = create_client(url, key)
return supabase
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"
return create_client(url, key)
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"