From c0aa9135c1a457c5ad00d3b143b5e2688ff940ef Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Thu, 28 Jan 2021 13:51:40 +0800 Subject: [PATCH 01/32] Initial commit --- LICENSE | 2 +- README.md | 14 ++++++++------ pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index ddeba6a0..8191ed13 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Supabase +Copyright (c) 2020 Joel Lee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c3d85f36..878359f5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # supabase-py -Supabase client for Python. +Supabase client for Python. This mirrors the design of [supabase-js](https://github.com/supabase/supabase-js/blob/master/README.md) + +## Usage + +`pip3 install gotrue` + ### See issues for what to work on Rough roadmap: - - [ ] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) -- [ ] Write Realtime-py (Use [realtime-js](https://github.com/supabase/realtime-js) as reference implementation) (implementation started by @Jeffery Kwoh @Joel and @Lionell Loh) -- [ ] Wrap Realtime-py (Use [supabase-js](https://github.com/supabase/supabase-js) as reference implementation) -- [ ] Write Gotrue-py (for auth) (Use [gotrue-js](https://github.com/netlify/gotrue-js) as reference implementation) -- [ ] Wrap Gotrue-py +- [ ] Wrap [Realtime-py](https://github.com/supabase/realtime-py) +- [ ] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) diff --git a/pyproject.toml b/pyproject.toml index 4dff867a..1d720ea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "supabase-py" version = "0.0.1" description = "Supabase client for Python." -authors = ["Lương Quang Mạnh "] +authors = ["Joel Lee "] license = "MIT" [tool.poetry.dependencies] From 09e731f97116bf2e698302f7ba2aac0968648e35 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Thu, 28 Jan 2021 16:50:25 +0800 Subject: [PATCH 02/32] Add method stubs --- README.md | 7 ++++++- supabase_py/client.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 supabase_py/client.py diff --git a/README.md b/README.md index 878359f5..e3908a4f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Supabase client for Python. This mirrors the design of [supabase-js](https://git ## Usage -`pip3 install gotrue` +`pip3 install supabase` ### See issues for what to work on @@ -13,3 +13,8 @@ Rough roadmap: - [ ] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) - [ ] Wrap [Realtime-py](https://github.com/supabase/realtime-py) - [ ] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) + + +### Client Library + +This is how you'd use [supabase-py] \ No newline at end of file diff --git a/supabase_py/client.py b/supabase_py/client.py new file mode 100644 index 00000000..5fdc440b --- /dev/null +++ b/supabase_py/client.py @@ -0,0 +1,43 @@ +class SupabaseClient(): + def __init__(self, supabaseUrl: str, supabaseKey: str): + if not supabaseUrl or not supabaseKey: + raise("supabaseUrl is required") + self.restUrl = f"{supabaseUrl}/rest/v1" + self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace('http', 'ws') + self.authUrl = f"{supabaseUrl}/auth/v1" + + + def some_other_stuff(self): + pass + + def auth(self): + pass + + def rpc(self): + + pass + + def removeSubscription(self): + pass + + def _closeSubscription(self, subscription): + pass + + def getSubscriptions(self): + pass + + def _initSupabaseAuthClient(self): + pass + + def _initSupabaseAuthClient(self): + pass + + def _initPostgRESTClient(self): + pass + + def _getAuthHeaders(self): + pass + + def _closeChannel(self): + pass + From adfb623ea8ab4fa1d8233b38abb86cfecd0ce740 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 29 Jan 2021 11:28:31 +0800 Subject: [PATCH 03/32] Add rpc function --- supabase_py/client.py | 49 +++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index 5fdc440b..4867b58c 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,21 +1,29 @@ +from postgrest_py import PostgrestClient + class SupabaseClient(): def __init__(self, supabaseUrl: str, supabaseKey: str): if not supabaseUrl or not supabaseKey: raise("supabaseUrl is required") + SETTINGS = {} self.restUrl = f"{supabaseUrl}/rest/v1" self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace('http', 'ws') self.authUrl = f"{supabaseUrl}/auth/v1" - - - def some_other_stuff(self): - pass + # TODO: Allow user to pass in schema. This is hardcoded + self.schema = 'public' + self.supabaseUrl = supabaseUrl + self.supabaseKey = supabaseKey + # self.auth = self._initSupabaseAuthClient(SETTINGS) def auth(self): pass - def rpc(self): + def rpc(self, fn, params): + """ + Performs a stored procedure call. + """ + rest = self._initPostgrestClient() + return rest.rpc(fn, params) - pass def removeSubscription(self): pass @@ -26,17 +34,36 @@ def _closeSubscription(self, subscription): def getSubscriptions(self): pass - def _initSupabaseAuthClient(self): + def _initRealtimeClient(self): pass + # return RealtimeClient(self.realtimeUrl, { + # params: { apiKey: self.supabaseKey} + # }) - def _initSupabaseAuthClient(self): + def _initSupabaseAuthClient(self, autoRefreshToken, persistSession, + detectSessionInUrl,localStorage): pass + # return SupabaseAuthClient({ + # self.authUrl, + # "headers": { + # "Authorization": f"Bearer {self.supabaseKey}", + # "apiKey": f"{self.supabaseKey}", + # autoRefreshToken, + # persistSession, + # detectSessionInUrl, + # localStorage + # } + # }) - def _initPostgRESTClient(self): - pass + def _initPostgrestClient(self): + return PostgrestClient(self.restUrl) def _getAuthHeaders(self): - pass + headers = {} + # authBearer = self.auth.session().token if self.auth.session().token else self.supabaseKey + headers['apiKey'] = self.supabaseKey + headers['Authorization'] = f"Bearer {self.supabaseKey}" + return headers def _closeChannel(self): pass From 3b0bb609052bf241990866f4937094442f3b87c5 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 30 Jan 2021 20:04:16 +0800 Subject: [PATCH 04/32] Update imports --- README.md | 10 ++++++++++ pyproject.toml | 5 ++++- supabase_py/__init__.py | 2 ++ supabase_py/client.py | 9 ++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e3908a4f..89a32c2f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # supabase-py +[![Documentation Status](https://readthedocs.org/projects/gotrue-py/badge/?version=latest)](https://gotrue-py.readthedocs.io/en/latest/?badge=latest) + Supabase client for Python. This mirrors the design of [supabase-js](https://github.com/supabase/supabase-js/blob/master/README.md) ## Usage @@ -7,6 +9,14 @@ Supabase client for Python. This mirrors the design of [supabase-js](https://git `pip3 install supabase` +``` +import supabase +supabaseUrl="" +supabaseKey="" +client = supabase.Client(supabaseUrl, supabaseKey) +``` + + ### See issues for what to work on Rough roadmap: diff --git a/pyproject.toml b/pyproject.toml index 1d720ea6..9ef9447d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,11 @@ authors = ["Joel Lee "] license = "MIT" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.7.1" postgrest-py = "^0.3.2" +realtime-py="^0.1.0" +gotrue="0.1.2" + [tool.poetry.dev-dependencies] diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index e69de29b..09ab9592 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -0,0 +1,2 @@ +from .lib import * +from .client import SupabaseClient \ No newline at end of file diff --git a/supabase_py/client.py b/supabase_py/client.py index 4867b58c..1f4a1caf 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,6 +1,7 @@ from postgrest_py import PostgrestClient +import gotrue -class SupabaseClient(): +class Client(): def __init__(self, supabaseUrl: str, supabaseKey: str): if not supabaseUrl or not supabaseKey: raise("supabaseUrl is required") @@ -14,6 +15,12 @@ def __init__(self, supabaseUrl: str, supabaseKey: str): self.supabaseKey = supabaseKey # self.auth = self._initSupabaseAuthClient(SETTINGS) + def _from(self, table: str): + """ + Perform a table operation + """ + pass + def auth(self): pass From f0f6d069d0fbda7bc4d73b6249d26ded98ed247c Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 30 Jan 2021 20:07:59 +0800 Subject: [PATCH 05/32] Add supporting files --- supabase_py/__init__.py | 4 ++-- supabase_py/src/SupabaseAuthClient.py | 3 +++ supabase_py/src/SupabaseQueryBuilder.py | 0 supabase_py/src/SupabaseRealtimeClient.py | 10 ++++++++++ supabase_py/src/__init__.py | 0 5 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 supabase_py/src/SupabaseAuthClient.py create mode 100644 supabase_py/src/SupabaseQueryBuilder.py create mode 100644 supabase_py/src/SupabaseRealtimeClient.py create mode 100644 supabase_py/src/__init__.py diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 09ab9592..fc4e4f5d 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,2 +1,2 @@ -from .lib import * -from .client import SupabaseClient \ No newline at end of file +from .src import * +from .client import Client \ No newline at end of file diff --git a/supabase_py/src/SupabaseAuthClient.py b/supabase_py/src/SupabaseAuthClient.py new file mode 100644 index 00000000..e4756ea5 --- /dev/null +++ b/supabase_py/src/SupabaseAuthClient.py @@ -0,0 +1,3 @@ +class SupabaseAuthClient: + def __init__(self): + pass diff --git a/supabase_py/src/SupabaseQueryBuilder.py b/supabase_py/src/SupabaseQueryBuilder.py new file mode 100644 index 00000000..e69de29b diff --git a/supabase_py/src/SupabaseRealtimeClient.py b/supabase_py/src/SupabaseRealtimeClient.py new file mode 100644 index 00000000..58a4dd35 --- /dev/null +++ b/supabase_py/src/SupabaseRealtimeClient.py @@ -0,0 +1,10 @@ +class SupabaseRealtimeClient: + def __init__(self): + pass + + def on(self): + pass + + def subscribe(self): + pass + \ No newline at end of file diff --git a/supabase_py/src/__init__.py b/supabase_py/src/__init__.py new file mode 100644 index 00000000..e69de29b From bd5d03b0cd389f468cdcb0c9e22840012ca18a5a Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 30 Jan 2021 20:52:41 +0800 Subject: [PATCH 06/32] Add auth client wrapper --- supabase_py/client.py | 45 +++++++++++------------ supabase_py/src/SupabaseAuthClient.py | 12 ++++-- supabase_py/src/SupabaseQueryBuilder.py | 7 ++++ supabase_py/src/SupabaseRealtimeClient.py | 3 +- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index 1f4a1caf..d9a79647 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,19 +1,34 @@ -from postgrest_py import PostgrestClient import gotrue -class Client(): +from postgrest_py import PostgrestClient +from .src.SupabaseAuthClient import SupabaseAuthClient +from .src.SupabaseRealtimeClient import SupabaseRealtimeClient +from .src.SupabaseQueryBuilder import SupabaseQueryBuilder + + +class Client: def __init__(self, supabaseUrl: str, supabaseKey: str): if not supabaseUrl or not supabaseKey: raise("supabaseUrl is required") - SETTINGS = {} + DEFAULT_HEADERS = {} + + SETTINGS = { + "schema": "public", + "autoRefreshToken": True, + "persistSession": True, + "detectSessionInUrl": True, + # TODO: Verify if the localStorage parameter is relevant in the python implementation + "localStorage": None, + "headers": DEFAULT_HEADERS, + } self.restUrl = f"{supabaseUrl}/rest/v1" self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace('http', 'ws') self.authUrl = f"{supabaseUrl}/auth/v1" # TODO: Allow user to pass in schema. This is hardcoded - self.schema = 'public' + self.schema = SETTINGS["schema"] self.supabaseUrl = supabaseUrl self.supabaseKey = supabaseKey - # self.auth = self._initSupabaseAuthClient(SETTINGS) + self.auth = self._initSupabaseAuthClient(*SETTINGS) def _from(self, table: str): """ @@ -21,9 +36,6 @@ def _from(self, table: str): """ pass - def auth(self): - pass - def rpc(self, fn, params): """ Performs a stored procedure call. @@ -43,30 +55,17 @@ def getSubscriptions(self): def _initRealtimeClient(self): pass - # return RealtimeClient(self.realtimeUrl, { - # params: { apiKey: self.supabaseKey} - # }) def _initSupabaseAuthClient(self, autoRefreshToken, persistSession, detectSessionInUrl,localStorage): - pass - # return SupabaseAuthClient({ - # self.authUrl, - # "headers": { - # "Authorization": f"Bearer {self.supabaseKey}", - # "apiKey": f"{self.supabaseKey}", - # autoRefreshToken, - # persistSession, - # detectSessionInUrl, - # localStorage - # } - # }) + return SupabaseAuthClient(self.authUrl, autoRefreshToken, persistSession, detectSessionInUrl, localStorage) def _initPostgrestClient(self): return PostgrestClient(self.restUrl) def _getAuthHeaders(self): headers = {} + # What's the corresponding method to get the token # authBearer = self.auth.session().token if self.auth.session().token else self.supabaseKey headers['apiKey'] = self.supabaseKey headers['Authorization'] = f"Bearer {self.supabaseKey}" diff --git a/supabase_py/src/SupabaseAuthClient.py b/supabase_py/src/SupabaseAuthClient.py index e4756ea5..84a95701 100644 --- a/supabase_py/src/SupabaseAuthClient.py +++ b/supabase_py/src/SupabaseAuthClient.py @@ -1,3 +1,9 @@ -class SupabaseAuthClient: - def __init__(self): - pass +import gotrue +class SupabaseAuthClient(gotrue.Client): + def __init__(self,authURL, headers=None, detectSessionInUrl=False, autoRefreshToken=False, persistSession=False, localStorage=None): + super().__init__(authURL) + self.headers = headers + self.detectSessionInUrl = detectSessionInUrl + self.autoRefreshToken = autoRefreshToken + self.persistSession = persistSession + self.localStorage = localStorage \ No newline at end of file diff --git a/supabase_py/src/SupabaseQueryBuilder.py b/supabase_py/src/SupabaseQueryBuilder.py index e69de29b..dd7422f5 100644 --- a/supabase_py/src/SupabaseQueryBuilder.py +++ b/supabase_py/src/SupabaseQueryBuilder.py @@ -0,0 +1,7 @@ +from postgrest_py import PostgrestClient +class SupabaseQueryBuilder: + def __init__(self): + pass + + def on(self): + pass \ No newline at end of file diff --git a/supabase_py/src/SupabaseRealtimeClient.py b/supabase_py/src/SupabaseRealtimeClient.py index 58a4dd35..c42b599a 100644 --- a/supabase_py/src/SupabaseRealtimeClient.py +++ b/supabase_py/src/SupabaseRealtimeClient.py @@ -6,5 +6,4 @@ def on(self): pass def subscribe(self): - pass - \ No newline at end of file + pass \ No newline at end of file From 2fc2747f109d28e27b8a01e5a803bff70f04eab2 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sun, 31 Jan 2021 20:55:54 +0800 Subject: [PATCH 07/32] Refactor and format with black --- supabase_py/__init__.py | 2 +- supabase_py/client.py | 60 +++++++++++++---------- supabase_py/src/SupabaseAuthClient.py | 14 +++++- supabase_py/src/SupabaseQueryBuilder.py | 11 +++-- supabase_py/src/SupabaseRealtimeClient.py | 4 +- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index fc4e4f5d..1a36b6ee 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,2 +1,2 @@ from .src import * -from .client import Client \ No newline at end of file +from .client import Client diff --git a/supabase_py/client.py b/supabase_py/client.py index d9a79647..bab3e0f2 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -6,25 +6,26 @@ from .src.SupabaseQueryBuilder import SupabaseQueryBuilder +DEFAULT_OPTIONS = { + "schema": "public", + "auto_refresh_token": True, + "persist_session": True, + "detect_session_in_url": True, + "headers": {}, +} + + class Client: def __init__(self, supabaseUrl: str, supabaseKey: str): - if not supabaseUrl or not supabaseKey: - raise("supabaseUrl is required") - DEFAULT_HEADERS = {} - - SETTINGS = { - "schema": "public", - "autoRefreshToken": True, - "persistSession": True, - "detectSessionInUrl": True, - # TODO: Verify if the localStorage parameter is relevant in the python implementation - "localStorage": None, - "headers": DEFAULT_HEADERS, - } + if not supabaseUrl: + raise Exception("supabaseUrl is required") + if not supabaseKey: + raise Exception("supabaseKey is required") + + settings = {**DEFAULT_OPTIONS, **options} self.restUrl = f"{supabaseUrl}/rest/v1" - self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace('http', 'ws') + self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace("http", "ws") self.authUrl = f"{supabaseUrl}/auth/v1" - # TODO: Allow user to pass in schema. This is hardcoded self.schema = SETTINGS["schema"] self.supabaseUrl = supabaseUrl self.supabaseKey = supabaseKey @@ -43,10 +44,9 @@ def rpc(self, fn, params): rest = self._initPostgrestClient() return rest.rpc(fn, params) - def removeSubscription(self): pass - + def _closeSubscription(self, subscription): pass @@ -56,21 +56,27 @@ def getSubscriptions(self): def _initRealtimeClient(self): pass - def _initSupabaseAuthClient(self, autoRefreshToken, persistSession, - detectSessionInUrl,localStorage): - return SupabaseAuthClient(self.authUrl, autoRefreshToken, persistSession, detectSessionInUrl, localStorage) - + def _initSupabaseAuthClient( + self, autoRefreshToken, persistSession, detectSessionInUrl, localStorage + ): + return SupabaseAuthClient( + self.authUrl, + autoRefreshToken, + persistSession, + detectSessionInUrl, + localStorage, + ) + def _initPostgrestClient(self): return PostgrestClient(self.restUrl) - - def _getAuthHeaders(self): + + def _getAuthHeaders(self): headers = {} # What's the corresponding method to get the token # authBearer = self.auth.session().token if self.auth.session().token else self.supabaseKey - headers['apiKey'] = self.supabaseKey - headers['Authorization'] = f"Bearer {self.supabaseKey}" + headers["apiKey"] = self.supabaseKey + headers["Authorization"] = f"Bearer {self.supabaseKey}" return headers - + def _closeChannel(self): pass - diff --git a/supabase_py/src/SupabaseAuthClient.py b/supabase_py/src/SupabaseAuthClient.py index 84a95701..7d84c1ec 100644 --- a/supabase_py/src/SupabaseAuthClient.py +++ b/supabase_py/src/SupabaseAuthClient.py @@ -1,9 +1,19 @@ import gotrue + + class SupabaseAuthClient(gotrue.Client): - def __init__(self,authURL, headers=None, detectSessionInUrl=False, autoRefreshToken=False, persistSession=False, localStorage=None): + def __init__( + self, + authURL, + headers=None, + detectSessionInUrl=False, + autoRefreshToken=False, + persistSession=False, + localStorage=None, + ): super().__init__(authURL) self.headers = headers self.detectSessionInUrl = detectSessionInUrl self.autoRefreshToken = autoRefreshToken self.persistSession = persistSession - self.localStorage = localStorage \ No newline at end of file + self.localStorage = localStorage diff --git a/supabase_py/src/SupabaseQueryBuilder.py b/supabase_py/src/SupabaseQueryBuilder.py index dd7422f5..83217f33 100644 --- a/supabase_py/src/SupabaseQueryBuilder.py +++ b/supabase_py/src/SupabaseQueryBuilder.py @@ -1,7 +1,10 @@ -from postgrest_py import PostgrestClient -class SupabaseQueryBuilder: - def __init__(self): +from postgrest_py.request_builder import RequestBuilder + + +class SupabaseQueryBuilder(RequestBuilder): + def __init__(self, session: AsyncClient, path: str): + super().__init__(session, path) pass def on(self): - pass \ No newline at end of file + pass diff --git a/supabase_py/src/SupabaseRealtimeClient.py b/supabase_py/src/SupabaseRealtimeClient.py index c42b599a..dcffb6e4 100644 --- a/supabase_py/src/SupabaseRealtimeClient.py +++ b/supabase_py/src/SupabaseRealtimeClient.py @@ -4,6 +4,6 @@ def __init__(self): def on(self): pass - + def subscribe(self): - pass \ No newline at end of file + pass From 20106ce2c0ff10d356cc179f1b597efebc0d5b38 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sun, 31 Jan 2021 22:06:36 +0800 Subject: [PATCH 08/32] Add _from functions, refactor --- README.md | 4 ++-- supabase_py/client.py | 27 ++++++++++++++++++----- supabase_py/src/SupabaseQueryBuilder.py | 17 +++++++++----- supabase_py/src/SupabaseRealtimeClient.py | 5 ++++- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 89a32c2f..a0702dfe 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ Supabase client for Python. This mirrors the design of [supabase-js](https://git ``` -import supabase +import supabase_py supabaseUrl="" supabaseKey="" -client = supabase.Client(supabaseUrl, supabaseKey) +client = supabase_py.Client(supabaseUrl, supabaseKey) ``` diff --git a/supabase_py/client.py b/supabase_py/client.py index bab3e0f2..9bcc20b4 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -4,6 +4,7 @@ from .src.SupabaseAuthClient import SupabaseAuthClient from .src.SupabaseRealtimeClient import SupabaseRealtimeClient from .src.SupabaseQueryBuilder import SupabaseQueryBuilder +from typing import Optional DEFAULT_OPTIONS = { @@ -16,7 +17,9 @@ class Client: - def __init__(self, supabaseUrl: str, supabaseKey: str): + def __init__( + self, supabaseUrl: str, supabaseKey: str, options: Optional[dict] = {} + ): if not supabaseUrl: raise Exception("supabaseUrl is required") if not supabaseKey: @@ -26,16 +29,25 @@ def __init__(self, supabaseUrl: str, supabaseKey: str): self.restUrl = f"{supabaseUrl}/rest/v1" self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace("http", "ws") self.authUrl = f"{supabaseUrl}/auth/v1" - self.schema = SETTINGS["schema"] + self.schema = settings["schema"] self.supabaseUrl = supabaseUrl self.supabaseKey = supabaseKey - self.auth = self._initSupabaseAuthClient(*SETTINGS) + self.auth = self._initSupabaseAuthClient(*settings) def _from(self, table: str): """ Perform a table operation """ - pass + url = f"{self.restUrl}/{table}" + return SupabaseQueryBuilder( + url, + { + "headers": self._getAuthHeaders(), + "schema": self.schema, + "realtime": self.realtime, + }, + table, + ) def rpc(self, fn, params): """ @@ -57,7 +69,12 @@ def _initRealtimeClient(self): pass def _initSupabaseAuthClient( - self, autoRefreshToken, persistSession, detectSessionInUrl, localStorage + self, + schema, + autoRefreshToken, + persistSession, + detectSessionInUrl, + localStorage, ): return SupabaseAuthClient( self.authUrl, diff --git a/supabase_py/src/SupabaseQueryBuilder.py b/supabase_py/src/SupabaseQueryBuilder.py index 83217f33..0e45006c 100644 --- a/supabase_py/src/SupabaseQueryBuilder.py +++ b/supabase_py/src/SupabaseQueryBuilder.py @@ -1,10 +1,15 @@ -from postgrest_py.request_builder import RequestBuilder +from postgrest_py.client import PostgrestClient +from .SupabaseRealtimeClient import SupabaseRealtimeClient -class SupabaseQueryBuilder(RequestBuilder): - def __init__(self, session: AsyncClient, path: str): - super().__init__(session, path) - pass +class SupabaseQueryBuilder(PostgrestClient): + def __init__(self, url, headers, schema, realtime, table): + super.__init__(url, {headers, schema}) + self._subscription = SupabaseRealtimeClient(realtime, schema, table) + self._realtime = realtime - def on(self): + def on(self, event, callback): + """ + Subscribe to realtime changes in your database + """ pass diff --git a/supabase_py/src/SupabaseRealtimeClient.py b/supabase_py/src/SupabaseRealtimeClient.py index dcffb6e4..564beed7 100644 --- a/supabase_py/src/SupabaseRealtimeClient.py +++ b/supabase_py/src/SupabaseRealtimeClient.py @@ -1,5 +1,8 @@ class SupabaseRealtimeClient: - def __init__(self): + def __init__(self, socket, schema, tableName): + pass + + def getPayloadRecords(payload): pass def on(self): From afa8189cb82257130fee9f4f48a8907560a91b4f Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Tue, 2 Feb 2021 14:59:11 +0800 Subject: [PATCH 09/32] Rename files to align with python convention --- supabase_py/__init__.py | 2 +- supabase_py/client.py | 7 +++---- supabase_py/src/SupabaseAuthClient.py | 19 ------------------- supabase_py/src/SupabaseQueryBuilder.py | 15 --------------- supabase_py/src/SupabaseRealtimeClient.py | 12 ------------ supabase_py/src/__init__.py | 0 tests/test_dummy.py | 6 ++++++ 7 files changed, 10 insertions(+), 51 deletions(-) delete mode 100644 supabase_py/src/SupabaseAuthClient.py delete mode 100644 supabase_py/src/SupabaseQueryBuilder.py delete mode 100644 supabase_py/src/SupabaseRealtimeClient.py delete mode 100644 supabase_py/src/__init__.py diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 1a36b6ee..7bc294f6 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,2 +1,2 @@ -from .src import * +from .lib import * from .client import Client diff --git a/supabase_py/client.py b/supabase_py/client.py index 9bcc20b4..8b4c99c3 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,9 +1,9 @@ import gotrue from postgrest_py import PostgrestClient -from .src.SupabaseAuthClient import SupabaseAuthClient -from .src.SupabaseRealtimeClient import SupabaseRealtimeClient -from .src.SupabaseQueryBuilder import SupabaseQueryBuilder +from .lib.supabase_auth_client import SupabaseAuthClient +from .lib.supabase_realtime_client import SupabaseRealtimeClient +from .lib.supabase_query_builder import SupabaseQueryBuilder from typing import Optional @@ -90,7 +90,6 @@ def _initPostgrestClient(self): def _getAuthHeaders(self): headers = {} # What's the corresponding method to get the token - # authBearer = self.auth.session().token if self.auth.session().token else self.supabaseKey headers["apiKey"] = self.supabaseKey headers["Authorization"] = f"Bearer {self.supabaseKey}" return headers diff --git a/supabase_py/src/SupabaseAuthClient.py b/supabase_py/src/SupabaseAuthClient.py deleted file mode 100644 index 7d84c1ec..00000000 --- a/supabase_py/src/SupabaseAuthClient.py +++ /dev/null @@ -1,19 +0,0 @@ -import gotrue - - -class SupabaseAuthClient(gotrue.Client): - def __init__( - self, - authURL, - headers=None, - detectSessionInUrl=False, - autoRefreshToken=False, - persistSession=False, - localStorage=None, - ): - super().__init__(authURL) - self.headers = headers - self.detectSessionInUrl = detectSessionInUrl - self.autoRefreshToken = autoRefreshToken - self.persistSession = persistSession - self.localStorage = localStorage diff --git a/supabase_py/src/SupabaseQueryBuilder.py b/supabase_py/src/SupabaseQueryBuilder.py deleted file mode 100644 index 0e45006c..00000000 --- a/supabase_py/src/SupabaseQueryBuilder.py +++ /dev/null @@ -1,15 +0,0 @@ -from postgrest_py.client import PostgrestClient -from .SupabaseRealtimeClient import SupabaseRealtimeClient - - -class SupabaseQueryBuilder(PostgrestClient): - def __init__(self, url, headers, schema, realtime, table): - super.__init__(url, {headers, schema}) - self._subscription = SupabaseRealtimeClient(realtime, schema, table) - self._realtime = realtime - - def on(self, event, callback): - """ - Subscribe to realtime changes in your database - """ - pass diff --git a/supabase_py/src/SupabaseRealtimeClient.py b/supabase_py/src/SupabaseRealtimeClient.py deleted file mode 100644 index 564beed7..00000000 --- a/supabase_py/src/SupabaseRealtimeClient.py +++ /dev/null @@ -1,12 +0,0 @@ -class SupabaseRealtimeClient: - def __init__(self, socket, schema, tableName): - pass - - def getPayloadRecords(payload): - pass - - def on(self): - pass - - def subscribe(self): - pass diff --git a/supabase_py/src/__init__.py b/supabase_py/src/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 5e20bfd9..7c50e90d 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -1,2 +1,8 @@ +import supabase_py def test_dummy(): + # Test auth component assert True == True + + +def test_client_initialziation(): + client = supabase_py.Client("http://testwebsite.com", "atestapi") From 2a9c171e0dcd42ec8fb381d30767aad7f96207f8 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 3 Feb 2021 15:12:46 +0800 Subject: [PATCH 10/32] Add realtime methods --- .gitignore | 1 - supabase_py/lib/__init__.py | 0 supabase_py/lib/supabase_auth_client.py | 19 +++++++++ supabase_py/lib/supabase_query_builder.py | 18 ++++++++ supabase_py/lib/supabase_realtime_client.py | 47 +++++++++++++++++++++ tests/test_dummy.py | 9 +++- 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 supabase_py/lib/__init__.py create mode 100644 supabase_py/lib/supabase_auth_client.py create mode 100644 supabase_py/lib/supabase_query_builder.py create mode 100644 supabase_py/lib/supabase_realtime_client.py diff --git a/.gitignore b/.gitignore index b6e47617..7114a35b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/supabase_py/lib/__init__.py b/supabase_py/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/supabase_py/lib/supabase_auth_client.py b/supabase_py/lib/supabase_auth_client.py new file mode 100644 index 00000000..7d84c1ec --- /dev/null +++ b/supabase_py/lib/supabase_auth_client.py @@ -0,0 +1,19 @@ +import gotrue + + +class SupabaseAuthClient(gotrue.Client): + def __init__( + self, + authURL, + headers=None, + detectSessionInUrl=False, + autoRefreshToken=False, + persistSession=False, + localStorage=None, + ): + super().__init__(authURL) + self.headers = headers + self.detectSessionInUrl = detectSessionInUrl + self.autoRefreshToken = autoRefreshToken + self.persistSession = persistSession + self.localStorage = localStorage diff --git a/supabase_py/lib/supabase_query_builder.py b/supabase_py/lib/supabase_query_builder.py new file mode 100644 index 00000000..fa109bc8 --- /dev/null +++ b/supabase_py/lib/supabase_query_builder.py @@ -0,0 +1,18 @@ +from postgrest_py.client import PostgrestClient +from .supabase_realtime_client import SupabaseRealtimeClient +from typing import Callable + + +class SupabaseQueryBuilder(PostgrestClient): + def __init__(self, url, headers, schema, realtime, table): + super.__init__(url, schema) + self._subscription = SupabaseRealtimeClient(realtime, schema, table) + self._realtime = realtime + + def on(self, event, callback): + """ + Subscribe to realtime changes in your database + """ + if not self._realtime.connected: + self._realtime.connect() + return self._subscription.on(event, callback) diff --git a/supabase_py/lib/supabase_realtime_client.py b/supabase_py/lib/supabase_realtime_client.py new file mode 100644 index 00000000..7b88e644 --- /dev/null +++ b/supabase_py/lib/supabase_realtime_client.py @@ -0,0 +1,47 @@ +from typing import Callable, Any + + +class SupabaseRealtimeClient: + def __init__(self, socket, schema, tableName): + topic = ( + f"realtime:{schema}" + if table_name == "*" + else f"realtime:{schema}:{tableName}" + ) + self.subscription = socket.set_channel(topic) + + def getPayloadRecords(self, payload: Any): + records = {"new": {}, "old": {}} + # TODO: Figure out how to create payload + # if payload.type == "INSERT" or payload.type == "UPDATE": + # records.new = Transformers.convertChangeData(payload.columns, payload.record) + # if (payload.type === 'UPDATE' || payload.type === 'DELETE'): + # records.old = Transformers.convertChangeData(payload.columns, payload.old_record) + return records + + def on(self, event, callback: Callable[..., Any]): + def cb(payload): + enriched_payload = { + "schema": payload.schema, + "table": payload.table, + "commit_timestamp": payload.commit_timestamp, + "event_type": payload.type, + "new": {}, + "old": {}, + } + enriched_payload = {**enriched_payload, **self.getPayloadRecords(payload)} + callback(enriched_payload) + + self.subscription.join().on(event, cb) + return self + + def subscribe(self, callback: Callable[..., Any]): + # TODO: Handle state change callbacks for error and close + self.subscription.join().on("ok", callback("SUBSCRIBED")) + self.subscription.join().on( + "error", lambda x: callback("SUBSCRIPTION_ERROR", x) + ) + self.subscription.join().on( + "timeout", lambda: callback("RETRYING_AFTER_TIMEOUT") + ) + return self.subscription diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 7c50e90d..5f0d90cf 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -1,8 +1,13 @@ +import pytest + +import sys +print(sys.path) + import supabase_py + def test_dummy(): # Test auth component assert True == True - def test_client_initialziation(): - client = supabase_py.Client("http://testwebsite.com", "atestapi") + client = supabase_py.Client("http://testwebsite.com", "atestapi") \ No newline at end of file From 0884897bd400d6d130c06956237d86dbf8c5ec86 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 5 Feb 2021 11:42:57 +0800 Subject: [PATCH 11/32] Update README.md --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a0702dfe..2f206de5 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,70 @@ Supabase client for Python. This mirrors the design of [supabase-js](https://git import supabase_py supabaseUrl="" supabaseKey="" -client = supabase_py.Client(supabaseUrl, supabaseKey) +supabase = supabase_py.Client(supabaseUrl, supabaseKey) ``` +### Run tests +`python3 -m pytest` ### See issues for what to work on Rough roadmap: -- [ ] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) -- [ ] Wrap [Realtime-py](https://github.com/supabase/realtime-py) -- [ ] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) +- [x] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) +- [x] Wrap [Realtime-py](https://github.com/supabase/realtime-py) +- [x] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) + ### Client Library -This is how you'd use [supabase-py] \ No newline at end of file +This is a sample of how you'd use [supabase-py]. Functions and tests are WIP + + +## Authenticate +``` +supabase.auth.signUp({ + "email": 'example@email.com', + "password": 'example-password', +}) +``` + + +## Sign-in +``` +supabase.auth.signIn({ + "email": 'example@email.com', + "password": 'example-password', +}) +``` + + +## Sign-in(Auth provider). This is not supported yet +``` +supabase.auth.signIn({ + // provider can be 'github', 'google', 'gitlab', or 'bitbucket' + "provider": 'github' +}) +``` + + +## Managing Data +``` +supabase + .from('countries') + .select(" + name, + cities ( + name + ) + ") +``` + +## Realtime Changes +``` +mySubscription = supabase + .from('countries') + .on('*', lambda x: print(x)) + .subscribe() + ``` +See [Supabase Docs](https://supabase.io/docs/guides/client-libraries) for full list of examples \ No newline at end of file From 85c4b527efef21f8e78b8a34d1e08478739ca042 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 5 Feb 2021 13:31:38 +0800 Subject: [PATCH 12/32] Enable and manually test auth --- README.md | 4 +-- supabase_py/client.py | 38 +++++++++++++++++++------ supabase_py/lib/supabase_auth_client.py | 2 +- tests/test_dummy.py | 9 +++++- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2f206de5..c941a821 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ supabase = supabase_py.Client(supabaseUrl, supabaseKey) ### See issues for what to work on Rough roadmap: -- [x] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) -- [x] Wrap [Realtime-py](https://github.com/supabase/realtime-py) +- [] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) +- [] Wrap [Realtime-py](https://github.com/supabase/realtime-py) - [x] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) diff --git a/supabase_py/client.py b/supabase_py/client.py index 8b4c99c3..e0965012 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -33,6 +33,7 @@ def __init__( self.supabaseUrl = supabaseUrl self.supabaseKey = supabaseKey self.auth = self._initSupabaseAuthClient(*settings) + self.realtime = self._initRealtimeClient() def _from(self, table: str): """ @@ -56,17 +57,29 @@ def rpc(self, fn, params): rest = self._initPostgrestClient() return rest.rpc(fn, params) - def removeSubscription(self): - pass + # def removeSubscription(self, subscription): + # async def remove_subscription_helper(resolve): + # try: + # await self._closeSubscription(subscription) + # openSubscriptions = len(self.getSubscriptions()) + # if not openSubscriptions: + # error = await self.realtime.disconnect() + # if error: + # return {"error": None, "data": { openSubscriptions}} + # except Error as e: + # return {error} - def _closeSubscription(self, subscription): - pass + # return remove_subscription_helper(subscription) + + async def _closeSubscription(self, subscription): + if not subscription.closed: + await self._closeChannel(subscription) def getSubscriptions(self): - pass + return self.realtime.channels def _initRealtimeClient(self): - pass + return RealtimeClient(self.realtimeUrl, {"params": {apikey: self.supabaseKey}}) def _initSupabaseAuthClient( self, @@ -82,6 +95,10 @@ def _initSupabaseAuthClient( persistSession, detectSessionInUrl, localStorage, + headers={ + "Authorization": f"Bearer {self.supabaseKey}", + "apikey": f"{self.supabaseKey}", + }, ) def _initPostgrestClient(self): @@ -94,5 +111,10 @@ def _getAuthHeaders(self): headers["Authorization"] = f"Bearer {self.supabaseKey}" return headers - def _closeChannel(self): - pass + # def closeSubscription(self): + # if not subscription.closed: + # await self._closeChannel(subscription) + + # def _closeChannel(self, subscription): + # async def _closeChannelHelper(): + # subscription.unsubscribe().on('OK') diff --git a/supabase_py/lib/supabase_auth_client.py b/supabase_py/lib/supabase_auth_client.py index 7d84c1ec..588cc4ed 100644 --- a/supabase_py/lib/supabase_auth_client.py +++ b/supabase_py/lib/supabase_auth_client.py @@ -5,11 +5,11 @@ class SupabaseAuthClient(gotrue.Client): def __init__( self, authURL, - headers=None, detectSessionInUrl=False, autoRefreshToken=False, persistSession=False, localStorage=None, + headers=None, ): super().__init__(authURL) self.headers = headers diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 5f0d90cf..c9fa8ef5 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -5,9 +5,16 @@ import supabase_py +""" +Convert this flow into a test +client = supabase_py.Client("", "") +client.auth.sign_up({"email": "anemail@gmail.com", "password": "apassword"}) +""" + def test_dummy(): # Test auth component assert True == True def test_client_initialziation(): - client = supabase_py.Client("http://testwebsite.com", "atestapi") \ No newline at end of file + client = supabase_py.Client("http://testwebsite.com", "atestapi") + From e06b1437ad8af3ccf85c5064024682dba481244c Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sat, 6 Feb 2021 00:01:36 +0800 Subject: [PATCH 13/32] Document client and query builder --- supabase_py/client.py | 69 ++++++++++++++++++++++- supabase_py/lib/supabase_query_builder.py | 35 +++++++++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index e0965012..6dba2da8 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -20,6 +20,21 @@ class Client: def __init__( self, supabaseUrl: str, supabaseKey: str, options: Optional[dict] = {} ): + """ + Initialize a Supabase Client + Parameters + ---------- + SupabaseUrl + URL of the Supabase instance that we are acting on + SupabaseKey + API key for the Supabase instance that we are acting on + Options + Any other settings that we wish to override + + Returns + None + ------- + """ if not supabaseUrl: raise Exception("supabaseUrl is required") if not supabaseKey: @@ -33,11 +48,20 @@ def __init__( self.supabaseUrl = supabaseUrl self.supabaseKey = supabaseKey self.auth = self._initSupabaseAuthClient(*settings) - self.realtime = self._initRealtimeClient() + # TODO: Fix this once Realtime-py is working + # self.realtime = self._initRealtimeClient() def _from(self, table: str): """ - Perform a table operation + Perform a table operation on a given table + Parameters + ---------- + table + Name of table to execute operations on + Returns + ------- + SupabaseQueryBuilder + Wrapper for Postgrest-py client which we can perform operations(e.g. select/update) with """ url = f"{self.restUrl}/{table}" return SupabaseQueryBuilder( @@ -47,16 +71,31 @@ def _from(self, table: str): "schema": self.schema, "realtime": self.realtime, }, + self.schema, + self.realtime, table, ) def rpc(self, fn, params): """ Performs a stored procedure call. + + Parameters + ---------- + fn + The stored procedure call to be execured + params + Parameters passed into the stored procedure call + + Returns + ------- + Response + Returns the HTTP Response object which results from executing the call. """ rest = self._initPostgrestClient() return rest.rpc(fn, params) + # TODO: Fix this segment after realtime-py is working # def removeSubscription(self, subscription): # async def remove_subscription_helper(resolve): # try: @@ -72,13 +111,27 @@ def rpc(self, fn, params): # return remove_subscription_helper(subscription) async def _closeSubscription(self, subscription): + """ + Close a given subscription + + Parameters + ---------- + subscription + The name of the channel + """ if not subscription.closed: await self._closeChannel(subscription) def getSubscriptions(self): + """ + Return all channels the the client is subscribed to. + """ return self.realtime.channels def _initRealtimeClient(self): + """ + Private method for creating an instance of the realtime-py client. + """ return RealtimeClient(self.realtimeUrl, {"params": {apikey: self.supabaseKey}}) def _initSupabaseAuthClient( @@ -89,6 +142,9 @@ def _initSupabaseAuthClient( detectSessionInUrl, localStorage, ): + """ + Private helper method for creating a wrapped instance of the GoTrue Client. + """ return SupabaseAuthClient( self.authUrl, autoRefreshToken, @@ -102,15 +158,22 @@ def _initSupabaseAuthClient( ) def _initPostgrestClient(self): + """ + Private helper method for creating a wrapped instance of the Postgrest client. + """ return PostgrestClient(self.restUrl) def _getAuthHeaders(self): + """ + Helper method to get auth headers + """ headers = {} - # What's the corresponding method to get the token + # TODO: Add way of getting auth token headers["apiKey"] = self.supabaseKey headers["Authorization"] = f"Bearer {self.supabaseKey}" return headers + # TODO: Fix this segment after realtime-py is working # def closeSubscription(self): # if not subscription.closed: # await self._closeChannel(subscription) diff --git a/supabase_py/lib/supabase_query_builder.py b/supabase_py/lib/supabase_query_builder.py index fa109bc8..58d03c05 100644 --- a/supabase_py/lib/supabase_query_builder.py +++ b/supabase_py/lib/supabase_query_builder.py @@ -5,13 +5,44 @@ class SupabaseQueryBuilder(PostgrestClient): def __init__(self, url, headers, schema, realtime, table): - super.__init__(url, schema) + """ + Subscribe to realtime changes in your database. + + Parameters + ---------- + url + Base URL of the Supabase Instance that the client library is acting on + headers + authentication/authorization headers which are passed in. + schema + schema of table that we are building queries for + realtime + realtime-py client + table + Name of table to look out for operations on + Returns + ------- + None + """ + super().__init__(url) self._subscription = SupabaseRealtimeClient(realtime, schema, table) self._realtime = realtime def on(self, event, callback): """ - Subscribe to realtime changes in your database + Subscribe to realtime changes in your database. + + Parameters + ---------- + event + the event which we are looking out for. + callback + function to be execute when the event is received + + Returns + ------- + SupabaseRealtimeClient + Returns an instance of a SupabaseRealtimeClient to allow for chaining. """ if not self._realtime.connected: self._realtime.connect() From b1417b7c6b6cb91006edf7debd2c0342d38fa552 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 10:56:28 +0000 Subject: [PATCH 14/32] ignore vim tags --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7114a35b..82d87a83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +tags + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From 30b486a1915d04e8092ba4bcacd759c37e4f7297 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 10:56:42 +0000 Subject: [PATCH 15/32] add version to package --- supabase_py/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 7bc294f6..3b39aa8e 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,2 +1,5 @@ -from .lib import * -from .client import Client +from lib import supabase_auth_client, supabase_query_builder, supabase_realtime_client +from client import Client + + +__VERSION__ = "0.0.1" From 7edf954424075bac1f31b796144cbb62e4df6d49 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 10:57:03 +0000 Subject: [PATCH 16/32] add setup.py to enable "pip install -e . " installs --- setup.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..2f5b2f67 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +import supabase_py + +import setuptools + + +def get_package_description() -> str: + """Returns a description of this package from the markdown files.""" + with open("README.md", "r") as stream: + readme: str = stream.read() + return readme + + +setuptools.setup( + name="mitto", + version=supabase_py.__version__, + author="Joel Lee, Leon Fedden", + author_email="joel@joellee.org", + description="Supabase client for Python.", + long_description=get_package_description(), + long_description_content_type="text/markdown", + url="https://github.com/supabase/supabase-py", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">=3.7", +) From 97f9162763ea9f1b10c6f22c9763b900821b21d2 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 11:33:40 +0000 Subject: [PATCH 17/32] cleaning up a little and making more pythonic --- supabase_py/__init__.py | 7 +- supabase_py/client.py | 112 +++++++----------- supabase_py/lib/__init__.py | 3 + supabase_py/lib/auth_client.py | 24 ++++ ...base_query_builder.py => query_builder.py} | 0 ..._realtime_client.py => realtime_client.py} | 0 supabase_py/lib/supabase_auth_client.py | 19 --- 7 files changed, 75 insertions(+), 90 deletions(-) create mode 100644 supabase_py/lib/auth_client.py rename supabase_py/lib/{supabase_query_builder.py => query_builder.py} (100%) rename supabase_py/lib/{supabase_realtime_client.py => realtime_client.py} (100%) delete mode 100644 supabase_py/lib/supabase_auth_client.py diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 3b39aa8e..9457c5ae 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,5 +1,8 @@ -from lib import supabase_auth_client, supabase_query_builder, supabase_realtime_client +# Retain module level imports for structured imports in tests etc. +from . import lib +from . import client +# Open up the client as an easy import. from client import Client -__VERSION__ = "0.0.1" +__version__ = "0.0.1" diff --git a/supabase_py/client.py b/supabase_py/client.py index 6dba2da8..18da79fa 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,10 +1,11 @@ import gotrue from postgrest_py import PostgrestClient -from .lib.supabase_auth_client import SupabaseAuthClient -from .lib.supabase_realtime_client import SupabaseRealtimeClient -from .lib.supabase_query_builder import SupabaseQueryBuilder -from typing import Optional +from lib.auth_client import SupabaseAuthClient +from lib.realtime_client import SupabaseRealtimeClient +from lib.query_builder import SupabaseQueryBuilder + +from typing import Any, Dict DEFAULT_OPTIONS = { @@ -18,56 +19,41 @@ class Client: def __init__( - self, supabaseUrl: str, supabaseKey: str, options: Optional[dict] = {} + self, supabase_url: str, supabase_key: str, **options, ): - """ - Initialize a Supabase Client + """Instanciate the client. + Parameters ---------- - SupabaseUrl - URL of the Supabase instance that we are acting on - SupabaseKey - API key for the Supabase instance that we are acting on - Options - Any other settings that we wish to override - - Returns - None - ------- - """ - if not supabaseUrl: - raise Exception("supabaseUrl is required") - if not supabaseKey: - raise Exception("supabaseKey is required") - - settings = {**DEFAULT_OPTIONS, **options} - self.restUrl = f"{supabaseUrl}/rest/v1" - self.realtimeUrl = f"{supabaseUrl}/realtime/v1".replace("http", "ws") - self.authUrl = f"{supabaseUrl}/auth/v1" + supabase_url: str + The URL to the Supabase instance that should be connected to. + supabase_key: str + The API key to the Supabase instance that should be connected to. + **options + Any extra settings to be optionally specified - also see the + `DEFAULT_OPTIONS` dict. + """ + if not supabase_url: + raise Exception("supabase_url is required") + if not supabase_key: + raise Exception("supabase_key is required") + settings: Dict[str, Any] = {**DEFAULT_OPTIONS, **options} + self.restUrl = f"{supabase_url}/rest/v1" + self.realtimeUrl = f"{supabase_url}/realtime/v1".replace("http", "ws") + self.authUrl = f"{supabase_url}/auth/v1" self.schema = settings["schema"] - self.supabaseUrl = supabaseUrl - self.supabaseKey = supabaseKey - self.auth = self._initSupabaseAuthClient(*settings) - # TODO: Fix this once Realtime-py is working - # self.realtime = self._initRealtimeClient() + self.supabaseUrl = supabase_url + self.supabaseKey = supabase_key + self.auth = self._init_supabase_auth_client(*settings) + self.realtime = self._init_realtime_client() def _from(self, table: str): - """ - Perform a table operation on a given table - Parameters - ---------- - table - Name of table to execute operations on - Returns - ------- - SupabaseQueryBuilder - Wrapper for Postgrest-py client which we can perform operations(e.g. select/update) with - """ - url = f"{self.restUrl}/{table}" + """Perform a table operation.""" + url = f"{self.rest_url}/{table}" return SupabaseQueryBuilder( url, { - "headers": self._getAuthHeaders(), + "headers": self._get_auth_headers(), "schema": self.schema, "realtime": self.realtime, }, @@ -95,22 +81,19 @@ def rpc(self, fn, params): rest = self._initPostgrestClient() return rest.rpc(fn, params) - # TODO: Fix this segment after realtime-py is working - # def removeSubscription(self, subscription): # async def remove_subscription_helper(resolve): # try: - # await self._closeSubscription(subscription) - # openSubscriptions = len(self.getSubscriptions()) - # if not openSubscriptions: + # 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": { openSubscriptions}} - # except Error as e: - # return {error} - + # return {"error": None, "data": { open_subscriptions}} + # except Exception as e: + # raise e # return remove_subscription_helper(subscription) - async def _closeSubscription(self, subscription): + async def _close_subscription(self, subscription): """ Close a given subscription @@ -122,19 +105,19 @@ async def _closeSubscription(self, subscription): if not subscription.closed: await self._closeChannel(subscription) - def getSubscriptions(self): + def get_subscriptions(self): """ Return all channels the the client is subscribed to. """ return self.realtime.channels - def _initRealtimeClient(self): + def _init_realtime_client(self): """ Private method for creating an instance of the realtime-py client. """ return RealtimeClient(self.realtimeUrl, {"params": {apikey: self.supabaseKey}}) - def _initSupabaseAuthClient( + def _init_supabase_auth_client( self, schema, autoRefreshToken, @@ -157,13 +140,13 @@ def _initSupabaseAuthClient( }, ) - def _initPostgrestClient(self): + def _init_postgrest_client(self): """ Private helper method for creating a wrapped instance of the Postgrest client. """ return PostgrestClient(self.restUrl) - def _getAuthHeaders(self): + def _get_auth_headers(self): """ Helper method to get auth headers """ @@ -172,12 +155,3 @@ def _getAuthHeaders(self): headers["apiKey"] = self.supabaseKey headers["Authorization"] = f"Bearer {self.supabaseKey}" return headers - - # TODO: Fix this segment after realtime-py is working - # def closeSubscription(self): - # if not subscription.closed: - # await self._closeChannel(subscription) - - # def _closeChannel(self, subscription): - # async def _closeChannelHelper(): - # subscription.unsubscribe().on('OK') diff --git a/supabase_py/lib/__init__.py b/supabase_py/lib/__init__.py index e69de29b..286d8833 100644 --- a/supabase_py/lib/__init__.py +++ b/supabase_py/lib/__init__.py @@ -0,0 +1,3 @@ +from . import auth_client +from . import query_builder +from . import realtime_client diff --git a/supabase_py/lib/auth_client.py b/supabase_py/lib/auth_client.py new file mode 100644 index 00000000..2c3801e4 --- /dev/null +++ b/supabase_py/lib/auth_client.py @@ -0,0 +1,24 @@ +import gotrue + +from typing import Any, Dict, Optional + +# TODO(fedden): What are the correct types here? +class SupabaseAuthClient(gotrue.Client): + """SupabaseAuthClient""" + + def __init__( + self, + authURL: str, + detectSessionInUrl: bool = False, + autoRefreshToken: bool = False, + persistSession: bool = False, + localStorage: Optional[Dict[str, Any]] = None, + headers: Optional[Any] = None, + ): + """Instanciate SupabaseAuthClient instance.""" + super().__init__(authURL) + self.headers = headers + self.detectSessionInUrl = detectSessionInUrl + self.autoRefreshToken = autoRefreshToken + self.persistSession = persistSession + self.localStorage = localStorage diff --git a/supabase_py/lib/supabase_query_builder.py b/supabase_py/lib/query_builder.py similarity index 100% rename from supabase_py/lib/supabase_query_builder.py rename to supabase_py/lib/query_builder.py diff --git a/supabase_py/lib/supabase_realtime_client.py b/supabase_py/lib/realtime_client.py similarity index 100% rename from supabase_py/lib/supabase_realtime_client.py rename to supabase_py/lib/realtime_client.py diff --git a/supabase_py/lib/supabase_auth_client.py b/supabase_py/lib/supabase_auth_client.py deleted file mode 100644 index 588cc4ed..00000000 --- a/supabase_py/lib/supabase_auth_client.py +++ /dev/null @@ -1,19 +0,0 @@ -import gotrue - - -class SupabaseAuthClient(gotrue.Client): - def __init__( - self, - authURL, - detectSessionInUrl=False, - autoRefreshToken=False, - persistSession=False, - localStorage=None, - headers=None, - ): - super().__init__(authURL) - self.headers = headers - self.detectSessionInUrl = detectSessionInUrl - self.autoRefreshToken = autoRefreshToken - self.persistSession = persistSession - self.localStorage = localStorage From 9248cf207c8e8f2418b9148803b0d865f76ec78a Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 11:47:40 +0000 Subject: [PATCH 18/32] improve readme --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c941a821..cdb3c77f 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,37 @@ Supabase client for Python. This mirrors the design of [supabase-js](https://github.com/supabase/supabase-js/blob/master/README.md) -## Usage +## Installation -`pip3 install supabase` +**Recomended:** First activate your virtual environment, with your favourites system. For example, we like `poetry` and `conda`! +#### PyPi installation +Now install the package. +```bash +pip install supabase +``` +#### Local installation +You can also installing from after cloning this repo. Install like below to install in Development Mode, which means when you edit the source code the changes will be reflected in your python module. +```bash +pip install -e . +``` +## Usage ``` import supabase_py -supabaseUrl="" -supabaseKey="" -supabase = supabase_py.Client(supabaseUrl, supabaseKey) +supabase_url="" +supabase_key="" +supabase = supabase_py.Client(supabase_url, supabase_key) ``` ### Run tests -`python3 -m pytest` +`python -m pytest` ### See issues for what to work on Rough roadmap: -- [] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) -- [] Wrap [Realtime-py](https://github.com/supabase/realtime-py) +- [ ] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) +- [ ] Wrap [Realtime-py](https://github.com/supabase/realtime-py) - [x] Wrap [Gotrue-py](https://github.com/J0/gotrue-py) @@ -79,4 +90,4 @@ mySubscription = supabase .on('*', lambda x: print(x)) .subscribe() ``` -See [Supabase Docs](https://supabase.io/docs/guides/client-libraries) for full list of examples \ No newline at end of file +See [Supabase Docs](https://supabase.io/docs/guides/client-libraries) for full list of examples From 6c9e99c5e23f3a30715405acc828c362e36672ec Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 23:49:09 +0000 Subject: [PATCH 19/32] add shim --- setup.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/setup.py b/setup.py index 2f5b2f67..bac24a43 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,6 @@ -import supabase_py +#!/usr/bin/env python import setuptools - -def get_package_description() -> str: - """Returns a description of this package from the markdown files.""" - with open("README.md", "r") as stream: - readme: str = stream.read() - return readme - - -setuptools.setup( - name="mitto", - version=supabase_py.__version__, - author="Joel Lee, Leon Fedden", - author_email="joel@joellee.org", - description="Supabase client for Python.", - long_description=get_package_description(), - long_description_content_type="text/markdown", - url="https://github.com/supabase/supabase-py", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires=">=3.7", -) +if __name__ == "__main__": + setuptools.setup() From c1bc0b1cd826cd689b25461dbc549ed83286bf95 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 23:49:43 +0000 Subject: [PATCH 20/32] rm unused library --- supabase_py/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index 18da79fa..42c56c78 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,5 +1,3 @@ -import gotrue - from postgrest_py import PostgrestClient from lib.auth_client import SupabaseAuthClient from lib.realtime_client import SupabaseRealtimeClient From db71ab49e162ebdfcc7647d183fbd123016ff846 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Fri, 5 Feb 2021 23:50:32 +0000 Subject: [PATCH 21/32] change import --- supabase_py/lib/query_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/supabase_py/lib/query_builder.py b/supabase_py/lib/query_builder.py index 58d03c05..9c2f223a 100644 --- a/supabase_py/lib/query_builder.py +++ b/supabase_py/lib/query_builder.py @@ -1,6 +1,5 @@ from postgrest_py.client import PostgrestClient -from .supabase_realtime_client import SupabaseRealtimeClient -from typing import Callable +from .realtime_client import SupabaseRealtimeClient class SupabaseQueryBuilder(PostgrestClient): From 290bebbb497e62eec1bbdcdf98c1be21483d2897 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 00:23:25 +0000 Subject: [PATCH 22/32] add setuptools --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ef9447d..25299ee5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,5 +15,8 @@ gotrue="0.1.2" [tool.poetry.dev-dependencies] [build-system] -requires = ["poetry>=0.12"] +requires = [ + "poetry>=0.12", + "setuptools>=30.3.0,<50", +] build-backend = "poetry.masonry.api" From 20d24049c57ef02ce738bca92cf6e4c414be4f7e Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 01:32:11 +0000 Subject: [PATCH 23/32] stepping through code, slightly changing codebase to reflect python idioms, adding realtime-py as a depedancy --- pyproject.toml | 3 +- supabase_py/__init__.py | 3 +- supabase_py/client.py | 118 ++++++++++++++++------------- supabase_py/lib/auth_client.py | 20 ++--- supabase_py/lib/realtime_client.py | 9 ++- 5 files changed, 86 insertions(+), 67 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25299ee5..d73e2983 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ python = "^3.7.1" postgrest-py = "^0.3.2" realtime-py="^0.1.0" gotrue="0.1.2" - +pytest="6.2.2" +supabase-realtime-py="0.1.1a0" [tool.poetry.dev-dependencies] diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 9457c5ae..8641920c 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -1,8 +1,9 @@ # Retain module level imports for structured imports in tests etc. from . import lib from . import client + # Open up the client as an easy import. -from client import Client +from .client import Client __version__ = "0.0.1" diff --git a/supabase_py/client.py b/supabase_py/client.py index 42c56c78..e20634c1 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -1,7 +1,7 @@ from postgrest_py import PostgrestClient -from lib.auth_client import SupabaseAuthClient -from lib.realtime_client import SupabaseRealtimeClient -from lib.query_builder import SupabaseQueryBuilder +from supabase_py.lib.auth_client import SupabaseAuthClient +from supabase_py.lib.realtime_client import SupabaseRealtimeClient +from supabase_py.lib.query_builder import SupabaseQueryBuilder from typing import Any, Dict @@ -10,12 +10,14 @@ "schema": "public", "auto_refresh_token": True, "persist_session": True, - "detect_session_in_url": True, - "headers": {}, + "detect_session_url": True, + "local_storage": {}, } class Client: + """Supabase client class.""" + def __init__( self, supabase_url: str, supabase_key: str, **options, ): @@ -35,29 +37,37 @@ def __init__( raise Exception("supabase_url is required") if not supabase_key: raise Exception("supabase_key is required") - settings: Dict[str, Any] = {**DEFAULT_OPTIONS, **options} - self.restUrl = f"{supabase_url}/rest/v1" - self.realtimeUrl = f"{supabase_url}/realtime/v1".replace("http", "ws") - self.authUrl = f"{supabase_url}/auth/v1" - self.schema = settings["schema"] - self.supabaseUrl = supabase_url - self.supabaseKey = supabase_key - self.auth = self._init_supabase_auth_client(*settings) - self.realtime = self._init_realtime_client() - - def _from(self, table: str): + self.supabase_url = supabase_url + self.supabase_key = supabase_key + # Start with defaults, write headers and prioritise user overwrites. + settings: Dict[str, Any] = { + **DEFAULT_OPTIONS, + "headers": self._get_auth_headers(), + **options, + } + 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.schema: str = settings.pop("schema") + # Instanciate clients. + self.auth: SupabaseAuthClient = self._init_supabase_auth_client( + auth_url=self.auth_url, supabase_key=self.supabase_key, **settings, + ) + # self.realtime: SupabaseRealtimeClient = self._init_realtime_client( + # realtime_url=self.realtime_url, supabase_key=self.supabase_key, + # ) + self.postgrest: PostgrestClient = self._init_postgrest_client( + rest_url=self.rest_url + ) + + def _from(self, table: str) -> SupabaseQueryBuilder: """Perform a table operation.""" - url = f"{self.rest_url}/{table}" return SupabaseQueryBuilder( - url, - { - "headers": self._get_auth_headers(), - "schema": self.schema, - "realtime": self.realtime, - }, - self.schema, - self.realtime, - table, + url=f"{self.rest_url}/{table}", + headers=self._get_auth_headers(), + schema=self.schema, + realtime=self.realtime, + table=table, ) def rpc(self, fn, params): @@ -109,47 +119,53 @@ def get_subscriptions(self): """ return self.realtime.channels - def _init_realtime_client(self): + @staticmethod + def _init_realtime_client( + realtime_url: str, supabase_key: str + ) -> SupabaseRealtimeClient: """ Private method for creating an instance of the realtime-py client. """ - return RealtimeClient(self.realtimeUrl, {"params": {apikey: self.supabaseKey}}) + return SupabaseRealtimeClient( + realtime_url, {"params": {"apikey": supabase_key}} + ) + @staticmethod def _init_supabase_auth_client( - self, - schema, - autoRefreshToken, - persistSession, - detectSessionInUrl, - localStorage, - ): + auth_url: str, + supabase_key: str, + detect_session_url: bool, + auto_refresh_token: bool, + persist_session: bool, + local_storage: Dict[str, Any], + headers: Dict[str, str], + ) -> SupabaseAuthClient: """ Private helper method for creating a wrapped instance of the GoTrue Client. """ return SupabaseAuthClient( - self.authUrl, - autoRefreshToken, - persistSession, - detectSessionInUrl, - localStorage, - headers={ - "Authorization": f"Bearer {self.supabaseKey}", - "apikey": f"{self.supabaseKey}", - }, + auth_url=auth_url, + auto_refresh_token=auto_refresh_token, + detect_session_url=detect_session_url, + persist_session=persist_session, + local_storage=local_storage, + headers=headers, ) - def _init_postgrest_client(self): + @staticmethod + def _init_postgrest_client(rest_url: str) -> PostgrestClient: """ Private helper method for creating a wrapped instance of the Postgrest client. """ - return PostgrestClient(self.restUrl) + return PostgrestClient(rest_url) - def _get_auth_headers(self): + def _get_auth_headers(self) -> Dict[str, str]: """ Helper method to get auth headers """ - headers = {} - # TODO: Add way of getting auth token - headers["apiKey"] = self.supabaseKey - headers["Authorization"] = f"Bearer {self.supabaseKey}" + # What's the corresponding method to get the token + headers: Dict[str, str] = { + "apiKey": self.supabase_key, + "Authorization": f"Bearer {self.supabase_key}", + } return headers diff --git a/supabase_py/lib/auth_client.py b/supabase_py/lib/auth_client.py index 2c3801e4..528904f4 100644 --- a/supabase_py/lib/auth_client.py +++ b/supabase_py/lib/auth_client.py @@ -8,17 +8,17 @@ class SupabaseAuthClient(gotrue.Client): def __init__( self, - authURL: str, - detectSessionInUrl: bool = False, - autoRefreshToken: bool = False, - persistSession: bool = False, - localStorage: Optional[Dict[str, Any]] = None, + auth_url: str, + detect_session_url: bool = False, + auto_refresh_token: bool = False, + persist_session: bool = False, + local_storage: Optional[Dict[str, Any]] = None, headers: Optional[Any] = None, ): """Instanciate SupabaseAuthClient instance.""" - super().__init__(authURL) + super().__init__(auth_url) self.headers = headers - self.detectSessionInUrl = detectSessionInUrl - self.autoRefreshToken = autoRefreshToken - self.persistSession = persistSession - self.localStorage = localStorage + self.detect_session_url = detect_session_url + self.auto_refresh_token = auto_refresh_token + self.persist_session = persist_session + self.local_storage = local_storage diff --git a/supabase_py/lib/realtime_client.py b/supabase_py/lib/realtime_client.py index 7b88e644..6ef7a888 100644 --- a/supabase_py/lib/realtime_client.py +++ b/supabase_py/lib/realtime_client.py @@ -1,16 +1,17 @@ -from typing import Callable, Any +from realtime_py.connection import Socket class SupabaseRealtimeClient: - def __init__(self, socket, schema, tableName): + def __init__(self, socket, schema, table_name): + topic = ( f"realtime:{schema}" if table_name == "*" - else f"realtime:{schema}:{tableName}" + else f"realtime:{schema}:{table_name}" ) self.subscription = socket.set_channel(topic) - def getPayloadRecords(self, payload: Any): + def get_payload_records(self, payload: Any): records = {"new": {}, "old": {}} # TODO: Figure out how to create payload # if payload.type == "INSERT" or payload.type == "UPDATE": From d524e0c7f217cf0e445925123f91da8257965be0 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 01:46:42 +0000 Subject: [PATCH 24/32] tests pass --- supabase_py/lib/realtime_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/supabase_py/lib/realtime_client.py b/supabase_py/lib/realtime_client.py index 6ef7a888..f6363b52 100644 --- a/supabase_py/lib/realtime_client.py +++ b/supabase_py/lib/realtime_client.py @@ -1,3 +1,5 @@ +from typing import Any, Callable + from realtime_py.connection import Socket From 0739a2f9766efa2b271c157a7e18a7249fcd345b Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 10:42:23 +0000 Subject: [PATCH 25/32] remove whitespace --- supabase_py/lib/realtime_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/supabase_py/lib/realtime_client.py b/supabase_py/lib/realtime_client.py index f6363b52..b14bc69f 100644 --- a/supabase_py/lib/realtime_client.py +++ b/supabase_py/lib/realtime_client.py @@ -5,7 +5,6 @@ class SupabaseRealtimeClient: def __init__(self, socket, schema, table_name): - topic = ( f"realtime:{schema}" if table_name == "*" From 1df7fc9dbb351a67a9301d2e8baf252c2351bde6 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 10:43:41 +0000 Subject: [PATCH 26/32] ignore vim stuff --- .gitignore | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.gitignore b/.gitignore index 82d87a83..35f915c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,24 @@ tags +## Vim stuff +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ # Byte-compiled / optimized / DLL files __pycache__/ From 0b7a164386afebb0fdb7dd25f501e257eba615b0 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 13:24:32 +0000 Subject: [PATCH 27/32] add new tests --- supabase_py/__init__.py | 4 +-- supabase_py/client.py | 9 +++-- supabase_py/lib/auth_client.py | 27 +++++++++++++-- tests/test_client.py | 63 ++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 tests/test_client.py diff --git a/supabase_py/__init__.py b/supabase_py/__init__.py index 8641920c..2ac32a05 100644 --- a/supabase_py/__init__.py +++ b/supabase_py/__init__.py @@ -2,8 +2,8 @@ from . import lib from . import client -# Open up the client as an easy import. -from .client import Client +# Open up the client and function as an easy import. +from .client import Client, create_client __version__ = "0.0.1" diff --git a/supabase_py/client.py b/supabase_py/client.py index e20634c1..5893b2ab 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -160,12 +160,15 @@ def _init_postgrest_client(rest_url: str) -> PostgrestClient: return PostgrestClient(rest_url) def _get_auth_headers(self) -> Dict[str, str]: - """ - Helper method to get auth headers - """ + """Helper method to get auth headers.""" # What's the corresponding method to get the token headers: Dict[str, str] = { "apiKey": self.supabase_key, "Authorization": f"Bearer {self.supabase_key}", } return headers + + +def create_client(supabase_url: str, supabase_key: str, **options) -> Client: + """Create client function to instanciate supabase client like JS runtime.""" + return Client(supabase_url=supabase_url, supabase_key=supabase_key, **options) diff --git a/supabase_py/lib/auth_client.py b/supabase_py/lib/auth_client.py index 528904f4..96fb870a 100644 --- a/supabase_py/lib/auth_client.py +++ b/supabase_py/lib/auth_client.py @@ -1,8 +1,8 @@ +from typing import Any, Dict, Optional + import gotrue -from typing import Any, Dict, Optional -# TODO(fedden): What are the correct types here? class SupabaseAuthClient(gotrue.Client): """SupabaseAuthClient""" @@ -13,7 +13,7 @@ def __init__( auto_refresh_token: bool = False, persist_session: bool = False, local_storage: Optional[Dict[str, Any]] = None, - headers: Optional[Any] = None, + headers: Dict[str, str] = {}, ): """Instanciate SupabaseAuthClient instance.""" super().__init__(auth_url) @@ -22,3 +22,24 @@ def __init__( self.auto_refresh_token = auto_refresh_token self.persist_session = persist_session self.local_storage = local_storage + self.jwt = None + + def sign_in(self, email: str, password: str) -> Dict[str, Any]: + """Sign in with email and password.""" + response = super().sign_in(credentials={"email": email, "password": password}) + # TODO(fedden): Log JWT to self.jwt + return response.json() + + def sign_up(self, email: str, password: str) -> Dict[str, Any]: + """Sign up with email and password.""" + response = super().sign_up(credentials={"email": email, "password": password}) + # TODO(fedden): Log JWT to self.jwt + return response.json() + + def sign_out(self) -> Dict[str, Any]: + """Sign out of logged in user.""" + if self.jwt is None: + raise ValueError("Cannot sign out if not signed in.") + response = super().sign_out(jwt=self.jwt) + self.jwt = None + return response.json() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..02bfd819 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,63 @@ +import os +import random +import string +from typing import Any, Dict + +import pytest + +""" +Convert this flow into a test +client = supabase_py.Client("", "") +client.auth.sign_up({"email": "anemail@gmail.com", "password": "apassword"}) +""" + + +def _random_string(length: int = 10) -> str: + """Generate random string.""" + return "".join(random.choices(string.ascii_uppercase + string.digits, k=length)) + + +def _assert_authenticated_user(user: Dict[str, Any]): + """Raise assertion error if user is not logged in correctly.""" + assert user.get("id") is not None + assert user.get("aud") == "authenticated" + + +def _assert_unauthenticated_user(user: Dict[str, Any]): + """Raise assertion error if user is logged in correctly.""" + assert user.get("id") is not None + assert user.get("aud") == "authenticated" + + +@pytest.mark.xfail( + reason="None of these values should be able to instanciate a client object" +) +@pytest.mark.parametrize("url", ["", None, "valeefgpoqwjgpj", 139, -1, {}, []]) +@pytest.mark.parametrize("key", ["", None, "valeefgpoqwjgpj", 139, -1, {}, []]) +def test_incorrect_values_dont_instanciate_client(url: Any, key: Any): + """Ensure we can't instanciate client with nonesense values.""" + from supabase_py import create_client, Client + + _: Client = create_client(url, key) + + +def test_client_auth(): + """Ensure we can create an auth user, and login with it.""" + from supabase_py import create_client, Client + + url: str = os.environ.get("SUPABASE_TEST_URL") + key: str = os.environ.get("SUPABASE_TEST_KEY") + supabase: Client = create_client(url, key) + # Create a random user login email and password. + random_email: str = f"{_random_string(10)}@supamail.com" + random_password: str = _random_string(20) + # Sign up (and sign in). + user = supabase.auth.sign_up(email=random_email, password=random_password) + _assert_authenticated_user(user) + # Sign out. + user = supabase.auth.sign_out() + _assert_unauthenticated_user(user) + # Sign in (explicitly this time). + user = supabase.auth.sign_in(email=random_email, password=random_password) + _assert_authenticated_user(user) + From 97100ad33fc01c1feaddfbb45e82fa2d844ab056 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 15:39:07 +0000 Subject: [PATCH 28/32] improve documetnation and add test (doesnt pass yet) --- supabase_py/client.py | 81 +++++++++++++++++++++--------- supabase_py/lib/auth_client.py | 4 +- supabase_py/lib/realtime_client.py | 2 +- tests/test_client.py | 18 ++++--- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index 5893b2ab..6193dad3 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -53,41 +53,54 @@ def __init__( self.auth: SupabaseAuthClient = self._init_supabase_auth_client( auth_url=self.auth_url, supabase_key=self.supabase_key, **settings, ) + # TODO(fedden): 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 = None self.postgrest: PostgrestClient = self._init_postgrest_client( rest_url=self.rest_url ) - def _from(self, table: str) -> SupabaseQueryBuilder: - """Perform a table operation.""" + def table(self, table_name: str) -> SupabaseQueryBuilder: + """Perform a table operation. + + Note that the supabase client uses the `from` method, but in Python, + this is a reserved keyword so we have elected to use the name `table`. + Alternatively you can use the `._from()` method. + """ + return self._from(table_name) + + def _from(self, table_name: str) -> SupabaseQueryBuilder: + """Perform a table operation. + + See the `table` method. + """ return SupabaseQueryBuilder( - url=f"{self.rest_url}/{table}", + url=f"{self.rest_url}/{table_name}", headers=self._get_auth_headers(), schema=self.schema, realtime=self.realtime, - table=table, + table=table_name, ) def rpc(self, fn, params): - """ - Performs a stored procedure call. + """Performs a stored procedure call. Parameters ---------- - fn - The stored procedure call to be execured - params - Parameters passed into the stored procedure call + fn : callable + The stored procedure call to be executed. + params : dict of any + Parameters passed into the stored procedure call. Returns ------- Response - Returns the HTTP Response object which results from executing the call. + Returns the HTTP Response object which results from executing the + call. """ - rest = self._initPostgrestClient() - return rest.rpc(fn, params) + return self.postgrest.rpc(fn, params) # async def remove_subscription_helper(resolve): # try: @@ -102,8 +115,7 @@ def rpc(self, fn, params): # return remove_subscription_helper(subscription) async def _close_subscription(self, subscription): - """ - Close a given subscription + """Close a given subscription Parameters ---------- @@ -114,18 +126,14 @@ async def _close_subscription(self, subscription): await self._closeChannel(subscription) def get_subscriptions(self): - """ - Return all channels the the client is subscribed to. - """ + """Return all channels the 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. - """ + """Private method for creating an instance of the realtime-py client.""" return SupabaseRealtimeClient( realtime_url, {"params": {"apikey": supabase_key}} ) @@ -154,9 +162,7 @@ def _init_supabase_auth_client( @staticmethod def _init_postgrest_client(rest_url: str) -> PostgrestClient: - """ - Private helper method for creating a wrapped instance of the Postgrest client. - """ + """Private helper for creating an instance of the Postgrest client.""" return PostgrestClient(rest_url) def _get_auth_headers(self) -> Dict[str, str]: @@ -170,5 +176,30 @@ def _get_auth_headers(self) -> Dict[str, str]: def create_client(supabase_url: str, supabase_key: str, **options) -> Client: - """Create client function to instanciate supabase client like JS runtime.""" + """Create client function to instanciate supabase client like JS runtime. + + Parameters + ---------- + supabase_url: str + The URL to the Supabase instance that should be connected to. + supabase_key: str + The API key to the Supabase instance that should be connected to. + **options + Any extra settings to be optionally specified - also see the + `DEFAULT_OPTIONS` dict. + + Examples + -------- + Instanciating the client. + >>> import os + >>> from supabase_py import create_client, Client + >>> + >>> url: str = os.environ.get("SUPABASE_TEST_URL") + >>> key: str = os.environ.get("SUPABASE_TEST_KEY") + >>> supabase: Client = create_client(url, key) + + Returns + ------- + Client + """ return Client(supabase_url=supabase_url, supabase_key=supabase_key, **options) diff --git a/supabase_py/lib/auth_client.py b/supabase_py/lib/auth_client.py index 96fb870a..fac1052c 100644 --- a/supabase_py/lib/auth_client.py +++ b/supabase_py/lib/auth_client.py @@ -28,13 +28,13 @@ def sign_in(self, email: str, password: str) -> Dict[str, Any]: """Sign in with email and password.""" response = super().sign_in(credentials={"email": email, "password": password}) # TODO(fedden): Log JWT to self.jwt - return response.json() + return response def sign_up(self, email: str, password: str) -> Dict[str, Any]: """Sign up with email and password.""" response = super().sign_up(credentials={"email": email, "password": password}) # TODO(fedden): Log JWT to self.jwt - return response.json() + return response def sign_out(self) -> Dict[str, Any]: """Sign out of logged in user.""" diff --git a/supabase_py/lib/realtime_client.py b/supabase_py/lib/realtime_client.py index b14bc69f..8f5eae11 100644 --- a/supabase_py/lib/realtime_client.py +++ b/supabase_py/lib/realtime_client.py @@ -31,7 +31,7 @@ def cb(payload): "new": {}, "old": {}, } - enriched_payload = {**enriched_payload, **self.getPayloadRecords(payload)} + enriched_payload = {**enriched_payload, **self.get_payload_records(payload)} callback(enriched_payload) self.subscription.join().on(event, cb) diff --git a/tests/test_client.py b/tests/test_client.py index 02bfd819..22b54c59 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,12 +5,6 @@ import pytest -""" -Convert this flow into a test -client = supabase_py.Client("", "") -client.auth.sign_up({"email": "anemail@gmail.com", "password": "apassword"}) -""" - def _random_string(length: int = 10) -> str: """Generate random string.""" @@ -25,8 +19,7 @@ def _assert_authenticated_user(user: Dict[str, Any]): def _assert_unauthenticated_user(user: Dict[str, Any]): """Raise assertion error if user is logged in correctly.""" - assert user.get("id") is not None - assert user.get("aud") == "authenticated" + assert False @pytest.mark.xfail( @@ -61,3 +54,12 @@ def test_client_auth(): user = supabase.auth.sign_in(email=random_email, password=random_password) _assert_authenticated_user(user) + +def test_client_select(): + """Ensure we can select data from a table.""" + from supabase_py import create_client, Client + + url: str = os.environ.get("SUPABASE_TEST_URL") + key: str = os.environ.get("SUPABASE_TEST_KEY") + supabase: Client = create_client(url, key) + data = supabase.table("countries").select("*") From 4c13a4f6bb48f9dedd83eee0c1435bcd7eef7f8e Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 15:39:44 +0000 Subject: [PATCH 29/32] return dicts --- supabase_py/lib/auth_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supabase_py/lib/auth_client.py b/supabase_py/lib/auth_client.py index fac1052c..96fb870a 100644 --- a/supabase_py/lib/auth_client.py +++ b/supabase_py/lib/auth_client.py @@ -28,13 +28,13 @@ def sign_in(self, email: str, password: str) -> Dict[str, Any]: """Sign in with email and password.""" response = super().sign_in(credentials={"email": email, "password": password}) # TODO(fedden): Log JWT to self.jwt - return response + return response.json() def sign_up(self, email: str, password: str) -> Dict[str, Any]: """Sign up with email and password.""" response = super().sign_up(credentials={"email": email, "password": password}) # TODO(fedden): Log JWT to self.jwt - return response + return response.json() def sign_out(self) -> Dict[str, Any]: """Sign out of logged in user.""" From b641029966928ff2dc9882621afd8ddc5313aca7 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sat, 6 Feb 2021 15:54:08 +0000 Subject: [PATCH 30/32] improve documentation --- README.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cdb3c77f..c114781a 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,34 @@ You can also installing from after cloning this repo. Install like below to inst ```bash pip install -e . ``` + ## Usage -``` -import supabase_py -supabase_url="" -supabase_key="" -supabase = supabase_py.Client(supabase_url, supabase_key) +```python +import os +from supabase_py import create_client, Client + +url: str = os.environ.get("SUPABASE_URL") +key: str = os.environ.get("SUPABASE_KEY") +email = "abcdde@gmail.com" +password = "password" +supabase: Client = create_client(url, key) +user = supabase.auth.sign_up(email, password) ``` -### Run tests -`python -m pytest` +### Running Tests +Currently the test suites are in a state of flux. We are expanding our clients tests to ensure things are working, and for now can connect to this test instance, that is populated with the following table: +

+ +

-### See issues for what to work on +The above test database is a blank supabase instance that has populated the `countries` table with the built in countries script that can be found in the supabase UI. You can launch the test scripts and point to the above test database with the +```bash +SUPABASE_TEST_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMjYwOTMyMiwiZXhwIjoxOTI4MTg1MzIyfQ.XL9W5I_VRQ4iyQHVQmjG0BkwRfx6eVyYB3uAKcesukg" \ +SUPABASE_TEST_URL="https://tfsatoopsijgjhrqplra.supabase.co" \ +pytest -x +``` +### See issues for what to work on Rough roadmap: - [ ] Wrap [Postgrest-py](https://github.com/supabase/postgrest-py/) - [ ] Wrap [Realtime-py](https://github.com/supabase/realtime-py) @@ -40,10 +55,8 @@ Rough roadmap: ### Client Library - This is a sample of how you'd use [supabase-py]. Functions and tests are WIP - ## Authenticate ``` supabase.auth.signUp({ From 255bb71e3562562d703919d37aabe799b47f6ab6 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sun, 7 Feb 2021 02:01:32 +0000 Subject: [PATCH 31/32] more doc --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c114781a..a6c26458 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ pip install -e . ``` ## Usage +It's usually best practice to set your api key environment variables in some way that version control doesn't track them, e.g don't put them in your python modules! Set the key and url for the supabase instance in the shell, or better yet, use a dotenv file. Heres how to set the variables in the shell. +```bash +export SUPABASE_URL="my-url-to-my-awesome-supabase-instance" +export SUPABASE_KEY="my-supa-dupa-secret-supabase-api-key" +``` + +We can then read the keys in the python source code. ```python import os from supabase_py import create_client, Client From ccb88f8a9ea44472b3ab6f219fa1feed45a28185 Mon Sep 17 00:00:00 2001 From: leonfedden Date: Sun, 7 Feb 2021 02:01:46 +0000 Subject: [PATCH 32/32] spelling --- supabase_py/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supabase_py/client.py b/supabase_py/client.py index 6193dad3..700a2554 100644 --- a/supabase_py/client.py +++ b/supabase_py/client.py @@ -21,7 +21,7 @@ class Client: def __init__( self, supabase_url: str, supabase_key: str, **options, ): - """Instanciate the client. + """Instantiate the client. Parameters ---------- @@ -49,7 +49,7 @@ def __init__( self.realtime_url: str = f"{supabase_url}/realtime/v1".replace("http", "ws") self.auth_url: str = f"{supabase_url}/auth/v1" self.schema: str = settings.pop("schema") - # Instanciate clients. + # Instantiate clients. self.auth: SupabaseAuthClient = self._init_supabase_auth_client( auth_url=self.auth_url, supabase_key=self.supabase_key, **settings, )