apppy-sb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apppy_sb-0.1.0/.gitignore +28 -0
- apppy_sb-0.1.0/PKG-INFO +22 -0
- apppy_sb-0.1.0/README.md +0 -0
- apppy_sb-0.1.0/pyproject.toml +36 -0
- apppy_sb-0.1.0/sb.mk +23 -0
- apppy_sb-0.1.0/src/apppy/sb/__init__.py +0 -0
- apppy_sb-0.1.0/src/apppy/sb/auth.py +334 -0
- apppy_sb-0.1.0/src/apppy/sb/db.py +46 -0
- apppy_sb-0.1.0/src/apppy/sb/fs.py +762 -0
- apppy_sb-0.1.0/src/apppy/sb/fs_unit_test.py +268 -0
- apppy_sb-0.1.0/src/apppy/sb/identity/__init__.py +43 -0
- apppy_sb-0.1.0/src/apppy/sb/identity/errors.py +18 -0
- apppy_sb-0.1.0/src/apppy/sb/logflare.py +214 -0
- apppy_sb-0.1.0/src/apppy/sb/migrations.py +31 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
__generated__/
|
|
2
|
+
dist/
|
|
3
|
+
*.egg-info
|
|
4
|
+
.env
|
|
5
|
+
.env.*
|
|
6
|
+
*.env
|
|
7
|
+
!.env.ci
|
|
8
|
+
.file_store/
|
|
9
|
+
*.pid
|
|
10
|
+
.python-version
|
|
11
|
+
*.secrets
|
|
12
|
+
.secrets
|
|
13
|
+
*.tar.gz
|
|
14
|
+
*.test_output/
|
|
15
|
+
.test_output/
|
|
16
|
+
uv.lock
|
|
17
|
+
*.whl
|
|
18
|
+
|
|
19
|
+
# System files
|
|
20
|
+
__pycache__
|
|
21
|
+
.DS_Store
|
|
22
|
+
|
|
23
|
+
# Editor files
|
|
24
|
+
*.sublime-project
|
|
25
|
+
*.sublime-workspace
|
|
26
|
+
.vscode/*
|
|
27
|
+
!.vscode/settings.json
|
|
28
|
+
|
apppy_sb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apppy-sb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Supabase integrations for server development
|
|
5
|
+
Project-URL: Homepage, https://github.com/spals/apppy
|
|
6
|
+
Author: Tim Kral
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: apppy-auth>=0.1.0
|
|
12
|
+
Requires-Dist: apppy-db>=0.1.0
|
|
13
|
+
Requires-Dist: apppy-env>=0.1.0
|
|
14
|
+
Requires-Dist: apppy-fs>=0.1.0
|
|
15
|
+
Requires-Dist: apppy-generic>=0.1.0
|
|
16
|
+
Requires-Dist: apppy-logger>=0.1.0
|
|
17
|
+
Requires-Dist: apppy-queues>=0.1.0
|
|
18
|
+
Requires-Dist: argon2-cffi==25.1.0
|
|
19
|
+
Requires-Dist: fastapi-lifespan-manager==0.1.4
|
|
20
|
+
Requires-Dist: requests==2.32.3
|
|
21
|
+
Requires-Dist: supabase==2.15.3
|
|
22
|
+
Requires-Dist: tuspy==1.1.0
|
apppy_sb-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "apppy-sb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Supabase integrations for server development"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{ name = "Tim Kral" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"apppy-auth>=0.1.0",
|
|
19
|
+
"apppy-env>=0.1.0",
|
|
20
|
+
"apppy-db>=0.1.0",
|
|
21
|
+
"apppy-fs>=0.1.0",
|
|
22
|
+
"apppy-generic>=0.1.0",
|
|
23
|
+
"apppy-logger>=0.1.0",
|
|
24
|
+
"apppy-queues>=0.1.0",
|
|
25
|
+
"argon2-cffi==25.1.0",
|
|
26
|
+
"fastapi-lifespan-manager==0.1.4",
|
|
27
|
+
"requests==2.32.3",
|
|
28
|
+
"supabase==2.15.3",
|
|
29
|
+
"tuspy==1.1.0"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/spals/apppy"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/apppy"]
|
apppy_sb-0.1.0/sb.mk
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ifndef APPPY_SB_MK_INCLUDED
|
|
2
|
+
APPPY_SB_MK_INCLUDED := 1
|
|
3
|
+
SB_PKG_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|
4
|
+
|
|
5
|
+
.PHONY: sb sb-dev sb/build sb/clean sb/install sb/install-dev
|
|
6
|
+
|
|
7
|
+
sb: sb/clean sb/install
|
|
8
|
+
|
|
9
|
+
sb-dev: sb/clean sb/install-dev
|
|
10
|
+
|
|
11
|
+
sb/build:
|
|
12
|
+
cd $(SB_PKG_DIR) && uvx --from build pyproject-build
|
|
13
|
+
|
|
14
|
+
sb/clean:
|
|
15
|
+
cd $(SB_PKG_DIR) && rm -rf dist/ *.egg-info .venv
|
|
16
|
+
|
|
17
|
+
sb/install: sb/build
|
|
18
|
+
cd $(SB_PKG_DIR) && uv pip install dist/*.whl
|
|
19
|
+
|
|
20
|
+
sb/install-dev:
|
|
21
|
+
cd $(SB_PKG_DIR) && uv pip install -e .
|
|
22
|
+
|
|
23
|
+
endif # APPPY_SB_MK_INCLUDED
|
|
File without changes
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, get_args
|
|
5
|
+
|
|
6
|
+
import gotrue
|
|
7
|
+
import gotrue.errors
|
|
8
|
+
import gotrue.helpers
|
|
9
|
+
import psycopg
|
|
10
|
+
from argon2 import PasswordHasher
|
|
11
|
+
from fastapi_lifespan_manager import LifespanManager
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from supabase.client import AsyncClient as NativeSupabaseAsyncClient
|
|
14
|
+
from supabase.client import AsyncSupabaseAuthClient as NativeSupabaseAuthAsyncClient
|
|
15
|
+
|
|
16
|
+
# AsyncAuthClient as NativeSupabaseAuthAsyncClient,
|
|
17
|
+
from supabase.client import (
|
|
18
|
+
create_async_client,
|
|
19
|
+
)
|
|
20
|
+
from supabase.lib.client_options import AsyncClientOptions as NativeSupabaseAsyncClientOptions
|
|
21
|
+
|
|
22
|
+
from apppy.auth.errors.user import (
|
|
23
|
+
UserSessionRefreshMissingSessionError,
|
|
24
|
+
UserSignInInvalidCredentialsError,
|
|
25
|
+
UserSignInServerError,
|
|
26
|
+
UserSignOutServerError,
|
|
27
|
+
UserSignUpInvalidCredentialsError,
|
|
28
|
+
UserSignUpServerError,
|
|
29
|
+
UserSignUpTooManyRetriesError,
|
|
30
|
+
)
|
|
31
|
+
from apppy.db.postgres import PostgresClient
|
|
32
|
+
from apppy.env import EnvSettings
|
|
33
|
+
from apppy.logger import WithLogger
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SupabaseAuthSessionStorage(gotrue.AsyncSupportedStorage, WithLogger):
|
|
37
|
+
"""
|
|
38
|
+
A storage implementation for Supabase Auth that uses the FastAPI session storage.
|
|
39
|
+
|
|
40
|
+
This allows us to persist state across requests, which is required for OAuth flows.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, session):
|
|
44
|
+
self._session = session
|
|
45
|
+
|
|
46
|
+
async def get_item(self, key: str) -> str | None:
|
|
47
|
+
return self._session.get(key)
|
|
48
|
+
|
|
49
|
+
async def set_item(self, key: str, value: str) -> None:
|
|
50
|
+
self._session[key] = value
|
|
51
|
+
|
|
52
|
+
async def remove_item(self, key: str) -> None:
|
|
53
|
+
self._session.pop(key, None)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SupabaseAuthSettings(EnvSettings):
|
|
57
|
+
api_anon_key: str = Field(alias="APP_SUPABASE_AUTH_API_ANON_KEY")
|
|
58
|
+
api_key: str = Field(alias="APP_SUPABASE_AUTH_API_KEY", exclude=True)
|
|
59
|
+
api_url: str = Field(alias="APP_SUPABASE_AUTH_API_URL")
|
|
60
|
+
|
|
61
|
+
max_user_signup_retries: int = Field(
|
|
62
|
+
alias="APP_SUPABASE_AUTH_MAX_USER_SIGNUP_RETRIES", default=2
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SupabaseAuthAdmin(WithLogger):
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
settings: SupabaseAuthSettings,
|
|
70
|
+
lifespan: LifespanManager,
|
|
71
|
+
postgres_client: PostgresClient,
|
|
72
|
+
):
|
|
73
|
+
self._settings = settings
|
|
74
|
+
self._postgres_client = postgres_client
|
|
75
|
+
|
|
76
|
+
lifespan.add(self._auth_admin_async_client)
|
|
77
|
+
|
|
78
|
+
async def _auth_admin_async_client(self):
|
|
79
|
+
native_anon_async_client = await create_async_client(
|
|
80
|
+
supabase_url=self._settings.api_url,
|
|
81
|
+
supabase_key=self._settings.api_key,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._supabase_auth_admin = native_anon_async_client.auth.admin
|
|
85
|
+
yield {"supbase_auth_admin": self._supabase_auth_admin}
|
|
86
|
+
|
|
87
|
+
self._logger.info("Closing Supabase Auth Admin client")
|
|
88
|
+
await self._supabase_auth_admin.close()
|
|
89
|
+
|
|
90
|
+
def _normalize_result_row(self, row: dict) -> dict:
|
|
91
|
+
def _normalize_key(key: str):
|
|
92
|
+
if key == "raw_app_meta_data":
|
|
93
|
+
return "app_metadata"
|
|
94
|
+
elif key == "raw_user_meta_data":
|
|
95
|
+
return "user_metadata"
|
|
96
|
+
|
|
97
|
+
return key
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
_normalize_key(k): str(v) if isinstance(v, uuid.UUID | datetime.datetime) else v
|
|
101
|
+
for k, v in row.items()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async def create_user(self, attributes: gotrue.AdminUserAttributes) -> gotrue.User:
|
|
105
|
+
auth_user_query = """
|
|
106
|
+
SELECT *
|
|
107
|
+
FROM auth.users
|
|
108
|
+
WHERE email = %(email)s
|
|
109
|
+
LIMIT 1
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
result_set = await self._postgres_client.db_query_async(
|
|
113
|
+
auth_user_query, {"email": attributes["email"]}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if result_set is None or len(result_set) != 1:
|
|
117
|
+
user_resp = await self._supabase_auth_admin.create_user(attributes)
|
|
118
|
+
return user_resp.user
|
|
119
|
+
else:
|
|
120
|
+
user_dict = self._normalize_result_row(result_set[0])
|
|
121
|
+
user_resp = gotrue.helpers.parse_user_response(user_dict)
|
|
122
|
+
return user_resp.user
|
|
123
|
+
except psycopg.DatabaseError as e:
|
|
124
|
+
self._logger.exception("Lookup during create user encountered an api error")
|
|
125
|
+
raise UserSignUpServerError("auth_user_lookup_error") from e
|
|
126
|
+
except gotrue.errors.AuthApiError as e:
|
|
127
|
+
self._logger.exception(
|
|
128
|
+
"Create user encountered an auth api error",
|
|
129
|
+
extra={"error_name": e.name},
|
|
130
|
+
)
|
|
131
|
+
raise UserSignUpServerError(e.code) from e
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SupabaseAuth(WithLogger):
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
settings: SupabaseAuthSettings,
|
|
138
|
+
supabase_auth_admin: SupabaseAuthAdmin,
|
|
139
|
+
) -> None:
|
|
140
|
+
self._settings = settings
|
|
141
|
+
self._password_hasher = PasswordHasher()
|
|
142
|
+
self._supabase_auth_admin = supabase_auth_admin
|
|
143
|
+
|
|
144
|
+
async def auth_async_client(
|
|
145
|
+
self, session: dict[str, Any] | None = None
|
|
146
|
+
) -> NativeSupabaseAuthAsyncClient:
|
|
147
|
+
if session is not None:
|
|
148
|
+
client_options = NativeSupabaseAsyncClientOptions(
|
|
149
|
+
storage=SupabaseAuthSessionStorage(session)
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
client_options = NativeSupabaseAsyncClientOptions()
|
|
153
|
+
|
|
154
|
+
native_anon_async_client: NativeSupabaseAsyncClient = await create_async_client(
|
|
155
|
+
supabase_url=self._settings.api_url,
|
|
156
|
+
supabase_key=self._settings.api_anon_key,
|
|
157
|
+
options=client_options,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return native_anon_async_client.auth
|
|
161
|
+
|
|
162
|
+
def is_native_supabase_provider(self, provider: str) -> bool:
|
|
163
|
+
return provider in get_args(gotrue.Provider)
|
|
164
|
+
|
|
165
|
+
async def user_session_login_id_token(
|
|
166
|
+
self,
|
|
167
|
+
credentials: gotrue.SignInWithIdTokenCredentials,
|
|
168
|
+
) -> gotrue.AuthResponse:
|
|
169
|
+
try:
|
|
170
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client()
|
|
171
|
+
auth_resp: gotrue.AuthResponse = await auth_async_client.sign_in_with_id_token(
|
|
172
|
+
credentials
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return auth_resp
|
|
176
|
+
except gotrue.errors.AuthApiError as e:
|
|
177
|
+
self._logger.exception(
|
|
178
|
+
"Login with id token encountered an auth api error",
|
|
179
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
180
|
+
)
|
|
181
|
+
raise UserSignInServerError(e.code) from e
|
|
182
|
+
except gotrue.errors.CustomAuthError as e:
|
|
183
|
+
self._logger.exception(
|
|
184
|
+
"Login with id token encountered an auth custom error",
|
|
185
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
186
|
+
)
|
|
187
|
+
raise UserSignInServerError(e.code) from e
|
|
188
|
+
|
|
189
|
+
async def user_session_login_oauth_finalize(
|
|
190
|
+
self,
|
|
191
|
+
session: dict[str, Any],
|
|
192
|
+
params: gotrue.CodeExchangeParams,
|
|
193
|
+
) -> gotrue.AuthResponse:
|
|
194
|
+
try:
|
|
195
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client(session)
|
|
196
|
+
auth_resp: gotrue.AuthResponse = await auth_async_client.exchange_code_for_session(
|
|
197
|
+
params
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return auth_resp
|
|
201
|
+
except gotrue.errors.AuthApiError as e:
|
|
202
|
+
self._logger.exception(
|
|
203
|
+
"Oauth login finalize encountered an auth api error",
|
|
204
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
205
|
+
)
|
|
206
|
+
raise UserSignInServerError(e.code) from e
|
|
207
|
+
except gotrue.errors.CustomAuthError as e:
|
|
208
|
+
self._logger.exception(
|
|
209
|
+
"OAuth login finalize encountered an auth custom error",
|
|
210
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
211
|
+
)
|
|
212
|
+
raise UserSignInServerError(e.code) from e
|
|
213
|
+
|
|
214
|
+
async def user_session_login_oauth(
|
|
215
|
+
self,
|
|
216
|
+
session: dict[str, Any],
|
|
217
|
+
credentials: gotrue.SignInWithOAuthCredentials,
|
|
218
|
+
) -> gotrue.OAuthResponse:
|
|
219
|
+
try:
|
|
220
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client(session)
|
|
221
|
+
oauth_resp: gotrue.OAuthResponse = await auth_async_client.sign_in_with_oauth(
|
|
222
|
+
credentials
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return oauth_resp
|
|
226
|
+
except gotrue.errors.AuthApiError as e:
|
|
227
|
+
self._logger.exception(
|
|
228
|
+
"Login with oauth encountered an auth api error",
|
|
229
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
230
|
+
)
|
|
231
|
+
raise UserSignInServerError(e.code) from e
|
|
232
|
+
except gotrue.errors.CustomAuthError as e:
|
|
233
|
+
self._logger.exception(
|
|
234
|
+
"Login with oauth encountered an auth custom error",
|
|
235
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
236
|
+
)
|
|
237
|
+
raise UserSignInServerError(e.code) from e
|
|
238
|
+
|
|
239
|
+
async def user_session_login_password(
|
|
240
|
+
self, credentials: gotrue.SignInWithPasswordCredentials
|
|
241
|
+
) -> gotrue.AuthResponse:
|
|
242
|
+
try:
|
|
243
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client()
|
|
244
|
+
auth_resp: gotrue.AuthResponse = await auth_async_client.sign_in_with_password(
|
|
245
|
+
credentials
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return auth_resp
|
|
249
|
+
except gotrue.errors.AuthApiError as e:
|
|
250
|
+
if e.message == "Invalid login credentials":
|
|
251
|
+
raise UserSignInInvalidCredentialsError() from e
|
|
252
|
+
|
|
253
|
+
self._logger.exception(
|
|
254
|
+
"Login with password encountered an auth api error",
|
|
255
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
256
|
+
)
|
|
257
|
+
raise UserSignInServerError(e.code) from e
|
|
258
|
+
except gotrue.errors.CustomAuthError as e:
|
|
259
|
+
self._logger.exception(
|
|
260
|
+
"Login with password encountered an auth custom error",
|
|
261
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
262
|
+
)
|
|
263
|
+
raise UserSignInServerError(e.code) from e
|
|
264
|
+
|
|
265
|
+
async def user_session_logout(self, options: gotrue.SignOutOptions) -> None:
|
|
266
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client()
|
|
267
|
+
try:
|
|
268
|
+
await auth_async_client.sign_out(options)
|
|
269
|
+
except gotrue.errors.AuthError as e:
|
|
270
|
+
self._logger.exception(
|
|
271
|
+
"Logout encountered an auth error",
|
|
272
|
+
extra={"error_name": e.name, "error_code": e.code, "error_message": e.message},
|
|
273
|
+
)
|
|
274
|
+
raise UserSignOutServerError(e.code) from e
|
|
275
|
+
|
|
276
|
+
async def user_session_refresh(self, refresh_token: str) -> gotrue.AuthResponse:
|
|
277
|
+
try:
|
|
278
|
+
auth_async_client: NativeSupabaseAuthAsyncClient = await self.auth_async_client()
|
|
279
|
+
auth_resp: gotrue.AuthResponse = await auth_async_client.refresh_session(refresh_token)
|
|
280
|
+
return auth_resp
|
|
281
|
+
except gotrue.errors.AuthSessionMissingError as e:
|
|
282
|
+
raise UserSessionRefreshMissingSessionError() from e
|
|
283
|
+
|
|
284
|
+
async def user_sign_up(
|
|
285
|
+
self, credentials: gotrue.SignUpWithPasswordCredentials
|
|
286
|
+
) -> gotrue.AuthResponse:
|
|
287
|
+
attempt = 0
|
|
288
|
+
while attempt < self._settings.max_user_signup_retries:
|
|
289
|
+
try:
|
|
290
|
+
auth_async_client = await self.auth_async_client()
|
|
291
|
+
auth_resp: gotrue.AuthResponse = await auth_async_client.sign_up(credentials)
|
|
292
|
+
return auth_resp
|
|
293
|
+
except gotrue.errors.AuthRetryableError:
|
|
294
|
+
self._logger.warning(
|
|
295
|
+
"Encountered an AuthRetryableError with signing up a Supabase user. Retrying.",
|
|
296
|
+
extra={
|
|
297
|
+
"attempt": attempt,
|
|
298
|
+
"email": credentials.get("email"),
|
|
299
|
+
"phone": credentials.get("phone"),
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
attempt += 1
|
|
303
|
+
await asyncio.sleep(0.5)
|
|
304
|
+
except gotrue.errors.AuthInvalidCredentialsError as e:
|
|
305
|
+
self._logger.exception(
|
|
306
|
+
e.message,
|
|
307
|
+
extra={"email": credentials.get("email"), "phone": credentials.get("phone")},
|
|
308
|
+
)
|
|
309
|
+
raise UserSignUpInvalidCredentialsError() from e
|
|
310
|
+
except gotrue.errors.AuthApiError as e:
|
|
311
|
+
self._logger.exception(
|
|
312
|
+
"Sign up encountered an auth api error",
|
|
313
|
+
extra={"email": credentials.get("email"), "phone": credentials.get("phone")},
|
|
314
|
+
)
|
|
315
|
+
raise UserSignUpServerError(e.code) from e
|
|
316
|
+
|
|
317
|
+
self._logger.error(
|
|
318
|
+
"Too many retry attempts to sign up a Supabase user",
|
|
319
|
+
extra={
|
|
320
|
+
"attempts": attempt,
|
|
321
|
+
"email": credentials.get("email"),
|
|
322
|
+
"phone": credentials.get("phone"),
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
raise UserSignUpTooManyRetriesError()
|
|
326
|
+
|
|
327
|
+
async def user_sign_up_id_token(
|
|
328
|
+
self,
|
|
329
|
+
provider: str,
|
|
330
|
+
attributes: gotrue.AdminUserAttributes,
|
|
331
|
+
) -> None:
|
|
332
|
+
user: gotrue.User = await self._supabase_auth_admin.create_user(attributes)
|
|
333
|
+
if user is None:
|
|
334
|
+
raise UserSignUpServerError("user_creation_failed_in_sign_up_id_token")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from fastapi_lifespan_manager import LifespanManager
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
from supabase import AsyncClient as NativeSupabaseAsyncClient
|
|
4
|
+
from supabase import Client as NativeSupabaseClient
|
|
5
|
+
from supabase.client import create_async_client, create_client
|
|
6
|
+
|
|
7
|
+
from apppy.env import EnvSettings
|
|
8
|
+
from apppy.logger import WithLogger
|
|
9
|
+
|
|
10
|
+
_SUPABASE_MICRO_MAX_CONNS = 200
|
|
11
|
+
_SUPABASE_SMALL_MAX_CONNS = 400
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SupabaseClientSettings(EnvSettings):
|
|
15
|
+
api_anon_key: str = Field(alias="APP_SUPABASE_API_ANON_KEY")
|
|
16
|
+
api_key: str = Field(alias="APP_SUPABASE_API_KEY", exclude=True)
|
|
17
|
+
api_url: str = Field(alias="APP_SUPABASE_API_URL")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SupabaseClient(WithLogger):
|
|
21
|
+
def __init__(self, settings: SupabaseClientSettings, lifespan: LifespanManager) -> None:
|
|
22
|
+
self._settings = settings
|
|
23
|
+
self._native_internal_client: NativeSupabaseClient = create_client(
|
|
24
|
+
supabase_url=settings.api_url, supabase_key=settings.api_key
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
lifespan.add(self.__create_async_client)
|
|
28
|
+
|
|
29
|
+
async def __create_async_client(self):
|
|
30
|
+
self._logger.info("Creating native_internal_async_client")
|
|
31
|
+
self._native_internal_async_client = await create_async_client(
|
|
32
|
+
supabase_url=self._settings.api_url,
|
|
33
|
+
supabase_key=self._settings.api_key,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
yield {"native_internal_async_client": self._native_internal_async_client}
|
|
37
|
+
|
|
38
|
+
self._logger.info("Closing native_internal_async_client")
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def internal_client(self) -> NativeSupabaseClient:
|
|
42
|
+
return self._native_internal_client
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def internal_async_client(self) -> NativeSupabaseAsyncClient:
|
|
46
|
+
return self._native_internal_async_client
|