Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
41ee2dd
chore: upgrade dependencies
leynier Aug 21, 2022
896a40c
chore: implement script to sync config of infra
leynier Aug 21, 2022
5be3c33
chore: add sourcery config file
leynier Aug 21, 2022
07f897e
'Refactored by Sourcery' (#149)
sourcery-ai[bot] Aug 21, 2022
68ed829
chore: change made by pyupgrade pre-commit
leynier Aug 21, 2022
971b816
Merge branch 'main' into next
leynier Aug 31, 2022
6b15a50
chore: update dependencies
leynier Aug 31, 2022
6a200fe
chore: format docstring to avoid linter warning
leynier Oct 9, 2022
93aabbe
chore(deps): bump dev dependencies
leynier Oct 9, 2022
9f80bbc
Merge branch 'main' into next
leynier Oct 9, 2022
5148277
chore(deps): fix poetry lock file
leynier Oct 9, 2022
20b882c
'Refactored by Sourcery' (#172)
sourcery-ai[bot] Oct 9, 2022
4cb7713
refactor: migrate to implementation as similar as possible to the imp…
leynier Oct 18, 2022
4e3be99
Merge branch 'main' into next
leynier Oct 18, 2022
9e4a1f2
'Refactored by Sourcery' (#175)
sourcery-ai[bot] Oct 18, 2022
6ffa532
fix: implement the reset_password_email method
leynier Oct 18, 2022
332f782
fix: insert default content type header
leynier Oct 18, 2022
c3ff22a
feat: implement decode_jwt_payload in helpers
leynier Oct 21, 2022
4c2b443
feat: add mfa
leynier Oct 21, 2022
d5a0920
'Refactored by Sourcery' (#176)
sourcery-ai[bot] Oct 21, 2022
b714206
feat: mfa challenge and verify and refresh session
leynier Oct 29, 2022
20638fe
Merge remote-tracking branch 'remotes/origin/next' into next
leynier Oct 29, 2022
6fbd3de
chore: implement clients and utils for testing
leynier Oct 29, 2022
30fd751
tests: add tests for create user in admin api
leynier Oct 30, 2022
ec6618b
fix: use literal from typing extensions
leynier Oct 30, 2022
33756af
tests: add python 3.11 and update poetry version
leynier Oct 30, 2022
45afb35
fix: respect EXPIRY_MARGIN on getSession
leynier Oct 30, 2022
01385ea
chore: gen sync files
leynier Oct 30, 2022
a973a1c
fix: list users method of admin api
leynier Nov 5, 2022
8c076f0
test: add test to user fetch methods of admin api
leynier Nov 5, 2022
9db7ce6
fix: bugs in admin api and finish tests implementation
leynier Nov 7, 2022
fb033e4
fix: patch merge conflicts on next
Jan 29, 2023
08231ba
chore: remove PyGithub as dep
Jan 29, 2023
1682747
Merge branch 'main' of github.com:supabase-community/gotrue-py into j…
Jan 29, 2023
36e8d12
fix: add jwt package
Jan 29, 2023
34b8fb1
fix: regenerate lock
Jan 29, 2023
1e51fce
fix: move dependency
Jan 29, 2023
7f50501
fix: bump isort and python version
Jan 29, 2023
dc3573c
chore: update ci
Jan 29, 2023
58db1be
fix: reinstate 3.8
Jan 29, 2023
66f3f3a
Merge pull request #219 from supabase-community/j0/patch_conflicts_on…
J0 Jan 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: [3.8, 3.9, "3.10", "3.11"]
runs-on: ${{ matrix.os }}
steps:
- name: Clone Repository
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repos:
args: ["--fix=lf"]

- repo: https://github.com/pycqa/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
args:
Expand Down Expand Up @@ -40,13 +40,13 @@ repos:
- id: black

- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
rev: v2.37.3
hooks:
- id: pyupgrade
args: ["--py37-plus", "--keep-runtime-typing"]

- repo: https://github.com/commitizen-tools/commitizen
rev: v2.28.0
rev: v2.32.1
hooks:
- id: commitizen
stages: [commit-msg]
2 changes: 2 additions & 0 deletions .sourcery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
refactor:
python_version: '3.7'
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ clean_infra:
docker-compose down --remove-orphans &&\
docker system prune -a --volumes -f

sync_infra:
python scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra

run_tests: run_infra sleep tests

build_sync:
Expand Down
19 changes: 9 additions & 10 deletions gotrue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

__version__ = "0.5.4"

from ._async.api import AsyncGoTrueAPI
from ._async.client import AsyncGoTrueClient
from ._async.storage import AsyncMemoryStorage, AsyncSupportedStorage
from ._sync.api import SyncGoTrueAPI
from ._sync.client import SyncGoTrueClient
from ._sync.storage import SyncMemoryStorage, SyncSupportedStorage
from .types import *

Client = SyncGoTrueClient
GoTrueAPI = SyncGoTrueAPI
from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI # type: ignore # noqa: F401
from ._async.gotrue_client import AsyncGoTrueClient # type: ignore # noqa: F401
from ._async.storage import AsyncMemoryStorage # type: ignore # noqa: F401
from ._async.storage import AsyncSupportedStorage # type: ignore # noqa: F401
from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI # type: ignore # noqa: F401
from ._sync.gotrue_client import SyncGoTrueClient # type: ignore # noqa: F401
from ._sync.storage import SyncMemoryStorage # type: ignore # noqa: F401
from ._sync.storage import SyncSupportedStorage # type: ignore # noqa: F401
from .types import * # type: ignore # noqa: F401, F403
175 changes: 175 additions & 0 deletions gotrue/_async/gotrue_admin_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from __future__ import annotations

from typing import Dict, List, Union

from ..helpers import parse_link_response, parse_user_response
from ..http_clients import AsyncClient
from ..types import (
AdminUserAttributes,
AuthMFAAdminDeleteFactorParams,
AuthMFAAdminDeleteFactorResponse,
AuthMFAAdminListFactorsParams,
AuthMFAAdminListFactorsResponse,
GenerateLinkParams,
GenerateLinkResponse,
Options,
User,
UserResponse,
)
from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI
from .gotrue_base_api import AsyncGoTrueBaseAPI


class AsyncGoTrueAdminAPI(AsyncGoTrueBaseAPI):
def __init__(
self,
*,
url: str = "",
headers: Dict[str, str] = {},
http_client: Union[AsyncClient, None] = None,
) -> None:
AsyncGoTrueBaseAPI.__init__(
self,
url=url,
headers=headers,
http_client=http_client,
)
self.mfa = AsyncGoTrueAdminMFAAPI()
self.mfa.list_factors = self._list_factors
self.mfa.delete_factor = self._delete_factor

async def sign_out(self, jwt: str) -> None:
"""
Removes a logged-in session.
"""
return await self._request(
"POST",
"logout",
jwt=jwt,
no_resolve_json=True,
)

async def invite_user_by_email(
self,
email: str,
options: Options = {},
) -> UserResponse:
"""
Sends an invite link to an email address.
"""
return await self._request(
"POST",
"invite",
body={"email": email, "data": options.get("data")},
redirect_to=options.get("redirect_to"),
xform=parse_user_response,
)

async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkResponse:
"""
Generates email links and OTPs to be sent via a custom email provider.
"""
return await self._request(
"POST",
"admin/generate_link",
body={
"type": params.get("type"),
"email": params.get("email"),
"password": params.get("password"),
"new_email": params.get("new_email"),
"data": params.get("options", {}).get("data"),
},
redirect_to=params.get("options", {}).get("redirect_to"),
xform=parse_link_response,
)

# User Admin API

async def create_user(self, attributes: AdminUserAttributes) -> UserResponse:
"""
Creates a new user.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
"POST",
"admin/users",
body=attributes,
xform=parse_user_response,
)

async def list_users(self) -> List[User]:
"""
Get a list of users.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
"GET",
"admin/users",
xform=lambda data: [User.parse_obj(user) for user in data["users"]]
if "users" in data
else [],
)

async def get_user_by_id(self, uid: str) -> UserResponse:
"""
Get user by id.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
"GET",
f"admin/users/{uid}",
xform=parse_user_response,
)

async def update_user_by_id(
self,
uid: str,
attributes: AdminUserAttributes,
) -> UserResponse:
"""
Updates the user data.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
"PUT",
f"admin/users/{uid}",
body=attributes,
xform=parse_user_response,
)

async def delete_user(self, id: str) -> None:
"""
Delete a user. Requires a `service_role` key.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request("DELETE", f"admin/users/{id}")

async def _list_factors(
self,
params: AuthMFAAdminListFactorsParams,
) -> AuthMFAAdminListFactorsResponse:
return await self._request(
"GET",
f"admin/users/{params.get('user_id')}/factors",
xform=AuthMFAAdminListFactorsResponse.parse_obj,
)

async def _delete_factor(
self,
params: AuthMFAAdminDeleteFactorParams,
) -> AuthMFAAdminDeleteFactorResponse:
return await self._request(
"DELETE",
f"admin/users/{params.get('user_id')}/factors/{params.get('factor_id')}",
xform=AuthMFAAdminDeleteFactorResponse.parse_obj,
)
32 changes: 32 additions & 0 deletions gotrue/_async/gotrue_admin_mfa_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from ..types import (
AuthMFAAdminDeleteFactorParams,
AuthMFAAdminDeleteFactorResponse,
AuthMFAAdminListFactorsParams,
AuthMFAAdminListFactorsResponse,
)


class AsyncGoTrueAdminMFAAPI:
"""
Contains the full multi-factor authentication administration API.
"""

async def list_factors(
self,
params: AuthMFAAdminListFactorsParams,
) -> AuthMFAAdminListFactorsResponse:
"""
Lists all factors attached to a user.
"""
raise NotImplementedError() # pragma: no cover

async def delete_factor(
self,
params: AuthMFAAdminDeleteFactorParams,
) -> AuthMFAAdminDeleteFactorResponse:
"""
Deletes a factor on a user. This will log the user out of all active
sessions (if the deleted factor was verified). There's no need to delete
unverified factors.
"""
raise NotImplementedError() # pragma: no cover
118 changes: 118 additions & 0 deletions gotrue/_async/gotrue_base_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from __future__ import annotations

from typing import Any, Callable, Dict, TypeVar, Union, overload

from httpx import Response
from pydantic import BaseModel
from typing_extensions import Literal, Self

from ..helpers import handle_exception
from ..http_clients import AsyncClient

T = TypeVar("T")


class AsyncGoTrueBaseAPI:
def __init__(
self,
*,
url: str,
headers: Dict[str, str],
http_client: Union[AsyncClient, None],
):
self._url = url
self._headers = headers
self._http_client = http_client or AsyncClient()

async def __aenter__(self) -> Self:
return self

async def __aexit__(self, exc_t, exc_v, exc_tb) -> None:
await self.close()

async def close(self) -> None:
await self._http_client.aclose()

@overload
async def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
path: str,
*,
jwt: Union[str, None] = None,
redirect_to: Union[str, None] = None,
headers: Union[Dict[str, str], None] = None,
query: Union[Dict[str, str], None] = None,
body: Union[Any, None] = None,
no_resolve_json: Literal[False] = False,
xform: Callable[[Any], T],
) -> T:
... # pragma: no cover

@overload
async def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
path: str,
*,
jwt: Union[str, None] = None,
redirect_to: Union[str, None] = None,
headers: Union[Dict[str, str], None] = None,
query: Union[Dict[str, str], None] = None,
body: Union[Any, None] = None,
no_resolve_json: Literal[True],
xform: Callable[[Response], T],
) -> T:
... # pragma: no cover

@overload
async def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
path: str,
*,
jwt: Union[str, None] = None,
redirect_to: Union[str, None] = None,
headers: Union[Dict[str, str], None] = None,
query: Union[Dict[str, str], None] = None,
body: Union[Any, None] = None,
no_resolve_json: bool = False,
) -> None:
... # pragma: no cover

async def _request(
self,
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
path: str,
*,
jwt: Union[str, None] = None,
redirect_to: Union[str, None] = None,
headers: Union[Dict[str, str], None] = None,
query: Union[Dict[str, str], None] = None,
body: Union[Any, None] = None,
no_resolve_json: bool = False,
xform: Union[Callable[[Any], T], None] = None,
) -> Union[T, None]:
url = f"{self._url}/{path}"
headers = {**self._headers, **(headers or {})}
if "Content-Type" not in headers:
headers["Content-Type"] = "application/json;charset=UTF-8"
if jwt:
headers["Authorization"] = f"Bearer {jwt}"
query = query or {}
if redirect_to:
query["redirect_to"] = redirect_to
try:
response = await self._http_client.request(
method,
url,
headers=headers,
params=query,
json=body.dict() if isinstance(body, BaseModel) else body,
)
response.raise_for_status()
result = response if no_resolve_json else response.json()
if xform:
return xform(result)
except Exception as e:
raise handle_exception(e)
Loading