Skip to content

Commit 5c07a73

Browse files
authored
fix(auth): mypy auth (#1282)
1 parent 4de4010 commit 5c07a73

28 files changed

+1178
-1830
lines changed

src/auth/Makefile

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,57 @@
1-
tests: pytest
1+
help::
2+
@echo "Available commands"
3+
@echo " help -- (default) print this message"
4+
5+
tests: mypy pytest
6+
help::
7+
@echo " tests -- run all tests for supabase_auth"
28

39
pytest: start-infra
410
uv run --package supabase_auth pytest --cov=./ --cov-report=xml --cov-report=html -vv
511

12+
mypy:
13+
uv run --package supabase_auth mypy src/supabase_auth tests
14+
help::
15+
@echo " mypy -- run mypy on supabase_auth"
16+
617
start-infra:
718
cd infra &&\
819
docker compose down &&\
920
docker compose up -d
1021
sleep 2
22+
help::
23+
@echo " start-infra -- start containers for tests"
1124

1225
clean-infra:
1326
cd infra &&\
1427
docker compose down --remove-orphans &&\
1528
docker system prune -a --volumes -f
29+
help::
30+
@echo " clean-infra -- delete all stored information about the containers"
1631

1732
stop-infra:
1833
cd infra &&\
1934
docker compose down --remove-orphans
35+
help::
36+
@echo " stop-infra -- stop containers for tests"
2037

2138
sync-infra:
2239
uv run --package supabase_auth scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra
40+
help::
41+
@echo " sync-infra -- update locked versions for test containers"
2342

2443
build-sync:
2544
uv run --package supabase_auth scripts/run-unasync.py
45+
help::
46+
@echo " build-sync -- generate _sync from _async code"
2647

2748
clean:
2849
rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache
2950
rm -f .coverage coverage.xml
51+
help::
52+
@echo " clean -- clean intermediary files"
3053

3154
build:
3255
uv build --package supabase_auth
56+
help::
57+
@echo " build -- invoke uv build on supabase_auth package"

src/auth/infra/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ services:
2121
GOTRUE_LOG_LEVEL: DEBUG
2222
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
2323
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
24+
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
2425
GOTRUE_EXTERNAL_GOOGLE_ENABLED: 'true'
2526
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: 53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com
2627
GOTRUE_EXTERNAL_GOOGLE_SECRET: Sm3s8RE85rDcS36iMy8YjrpC
@@ -61,6 +62,7 @@ services:
6162
GOTRUE_LOG_LEVEL: DEBUG
6263
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
6364
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
65+
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
6466
GOTRUE_EXTERNAL_PHONE_ENABLED: 'true'
6567
GOTRUE_SMTP_HOST: mail
6668
GOTRUE_SMTP_PORT: 2500
@@ -90,6 +92,7 @@ services:
9092
GOTRUE_SMS_AUTOCONFIRM: 'true'
9193
GOTRUE_LOG_LEVEL: DEBUG
9294
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
95+
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
9396
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
9497
GOTRUE_EXTERNAL_PHONE_ENABLED: 'true'
9598
GOTRUE_SMTP_HOST: mail
@@ -119,6 +122,7 @@ services:
119122
GOTRUE_LOG_LEVEL: DEBUG
120123
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
121124
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
125+
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
122126
GOTRUE_EXTERNAL_PHONE_ENABLED: 'false'
123127
GOTRUE_EXTERNAL_EMAIL_ENABLED: 'false'
124128
GOTRUE_SMTP_HOST: mail

src/auth/pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ tests = [
4444
lints = [
4545
"ruff >=0.12.1",
4646
"unasync >= 0.6.0",
47+
"python-lsp-server (>=1.12.2,<2.0.0)",
48+
"pylsp-mypy (>=0.7.0,<0.8.0)",
49+
"python-lsp-ruff (>=2.2.2,<3.0.0)",
4750
]
4851
dev = [{ include-group = "lints" }, {include-group = "tests" }]
4952

@@ -76,3 +79,15 @@ asyncio_mode = "auto"
7679
[build-system]
7780
requires = ["uv_build>=0.8.3,<0.9.0"]
7881
build-backend = "uv_build"
82+
83+
[tool.mypy]
84+
python_version = "3.9"
85+
check_untyped_defs = true
86+
allow_redefinition = true
87+
follow_untyped_imports = true # for deprecation module that does not have stubs
88+
89+
no_warn_no_return = true
90+
warn_return_any = true
91+
warn_unused_configs = true
92+
warn_redundant_casts = true
93+
warn_unused_ignores = true

src/auth/scripts/run-unasync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import unasync
44

5-
paths = Path("../src/supabase").glob("**/*.py")
5+
paths = Path("src/supabase_auth").glob("**/*.py")
66
tests = Path("tests").glob("**/*.py")
77

88
rules = (unasync._DEFAULT_RULE,)
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
from __future__ import annotations
22

3-
from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI # type: ignore # noqa: F401
4-
from ._async.gotrue_client import AsyncGoTrueClient # type: ignore # noqa: F401
3+
from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI
4+
from ._async.gotrue_client import AsyncGoTrueClient
55
from ._async.storage import (
6-
AsyncMemoryStorage, # type: ignore # noqa: F401
7-
AsyncSupportedStorage, # type: ignore # noqa: F401
6+
AsyncMemoryStorage,
7+
AsyncSupportedStorage,
88
)
9-
from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI # type: ignore # noqa: F401
10-
from ._sync.gotrue_client import SyncGoTrueClient # type: ignore # noqa: F401
9+
from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI
10+
from ._sync.gotrue_client import SyncGoTrueClient
1111
from ._sync.storage import (
12-
SyncMemoryStorage, # type: ignore # noqa: F401
13-
SyncSupportedStorage, # type: ignore # noqa: F401
12+
SyncMemoryStorage,
13+
SyncSupportedStorage,
1414
)
15-
from .types import * # type: ignore # noqa: F401, F403
15+
from .types import *
1616
from .version import __version__

src/auth/src/supabase_auth/_async/gotrue_admin_api.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3-
from functools import partial
4-
from typing import Dict, List, Optional
3+
from typing import Any, Dict, List, Optional
4+
5+
from httpx import QueryParams, Response
6+
from pydantic import TypeAdapter
57

68
from ..helpers import (
79
is_valid_uuid,
@@ -21,6 +23,7 @@
2123
InviteUserByEmailOptions,
2224
SignOutScope,
2325
User,
26+
UserList,
2427
UserResponse,
2528
)
2629
from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI
@@ -45,18 +48,19 @@ def __init__(
4548
verify=verify,
4649
proxy=proxy,
4750
)
51+
# TODO(@o-santi): why is is this done this way?
4852
self.mfa = AsyncGoTrueAdminMFAAPI()
49-
self.mfa.list_factors = self._list_factors
50-
self.mfa.delete_factor = self._delete_factor
53+
self.mfa.list_factors = self._list_factors # type: ignore
54+
self.mfa.delete_factor = self._delete_factor # type: ignore
5155

5256
async def sign_out(self, jwt: str, scope: SignOutScope = "global") -> None:
5357
"""
5458
Removes a logged-in session.
5559
"""
56-
return await self._request(
60+
await self._request(
5761
"POST",
5862
"logout",
59-
query={"scope": scope},
63+
query=QueryParams(scope=scope),
6064
jwt=jwt,
6165
no_resolve_json=True,
6266
)
@@ -69,19 +73,19 @@ async def invite_user_by_email(
6973
"""
7074
Sends an invite link to an email address.
7175
"""
72-
return await self._request(
76+
response = await self._request(
7377
"POST",
7478
"invite",
7579
body={"email": email, "data": options.get("data")},
7680
redirect_to=options.get("redirect_to"),
77-
xform=parse_user_response,
7881
)
82+
return parse_user_response(response)
7983

8084
async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkResponse:
8185
"""
8286
Generates email links and OTPs to be sent via a custom email provider.
8387
"""
84-
return await self._request(
88+
response = await self._request(
8589
"POST",
8690
"admin/generate_link",
8791
body={
@@ -92,9 +96,10 @@ async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkRespons
9296
"data": params.get("options", {}).get("data"),
9397
},
9498
redirect_to=params.get("options", {}).get("redirect_to"),
95-
xform=parse_link_response,
9699
)
97100

101+
return parse_link_response(response)
102+
98103
# User Admin API
99104

100105
async def create_user(self, attributes: AdminUserAttributes) -> UserResponse:
@@ -104,30 +109,28 @@ async def create_user(self, attributes: AdminUserAttributes) -> UserResponse:
104109
This function should only be called on a server.
105110
Never expose your `service_role` key in the browser.
106111
"""
107-
return await self._request(
112+
response = await self._request(
108113
"POST",
109114
"admin/users",
110115
body=attributes,
111-
xform=parse_user_response,
112116
)
117+
return parse_user_response(response)
113118

114-
async def list_users(self, page: int = None, per_page: int = None) -> List[User]:
119+
async def list_users(
120+
self, page: Optional[int] = None, per_page: Optional[int] = None
121+
) -> List[User]:
115122
"""
116123
Get a list of users.
117124
118125
This function should only be called on a server.
119126
Never expose your `service_role` key in the browser.
120127
"""
121-
return await self._request(
128+
response = await self._request(
122129
"GET",
123130
"admin/users",
124-
query={"page": page, "per_page": per_page},
125-
xform=lambda data: (
126-
[model_validate(User, user) for user in data["users"]]
127-
if "users" in data
128-
else []
129-
),
131+
query=QueryParams(page=page, per_page=per_page),
130132
)
133+
return model_validate(UserList, response.content).users
131134

132135
async def get_user_by_id(self, uid: str) -> UserResponse:
133136
"""
@@ -138,11 +141,11 @@ async def get_user_by_id(self, uid: str) -> UserResponse:
138141
"""
139142
self._validate_uuid(uid)
140143

141-
return await self._request(
144+
response = await self._request(
142145
"GET",
143146
f"admin/users/{uid}",
144-
xform=parse_user_response,
145147
)
148+
return parse_user_response(response)
146149

147150
async def update_user_by_id(
148151
self,
@@ -156,12 +159,12 @@ async def update_user_by_id(
156159
Never expose your `service_role` key in the browser.
157160
"""
158161
self._validate_uuid(uid)
159-
return await self._request(
162+
response = await self._request(
160163
"PUT",
161164
f"admin/users/{uid}",
162165
body=attributes,
163-
xform=parse_user_response,
164166
)
167+
return parse_user_response(response)
165168

166169
async def delete_user(self, id: str, should_soft_delete: bool = False) -> None:
167170
"""
@@ -172,31 +175,33 @@ async def delete_user(self, id: str, should_soft_delete: bool = False) -> None:
172175
"""
173176
self._validate_uuid(id)
174177
body = {"should_soft_delete": should_soft_delete}
175-
return await self._request("DELETE", f"admin/users/{id}", body=body)
178+
await self._request("DELETE", f"admin/users/{id}", body=body)
176179

177180
async def _list_factors(
178181
self,
179182
params: AuthMFAAdminListFactorsParams,
180183
) -> AuthMFAAdminListFactorsResponse:
181184
self._validate_uuid(params.get("user_id"))
182-
return await self._request(
185+
response = await self._request(
183186
"GET",
184187
f"admin/users/{params.get('user_id')}/factors",
185-
xform=partial(model_validate, AuthMFAAdminListFactorsResponse),
186188
)
189+
return model_validate(AuthMFAAdminListFactorsResponse, response.content)
187190

188191
async def _delete_factor(
189192
self,
190193
params: AuthMFAAdminDeleteFactorParams,
191194
) -> AuthMFAAdminDeleteFactorResponse:
192195
self._validate_uuid(params.get("user_id"))
193196
self._validate_uuid(params.get("id"))
194-
return await self._request(
197+
response = await self._request(
195198
"DELETE",
196199
f"admin/users/{params.get('user_id')}/factors/{params.get('id')}",
197-
xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse),
198200
)
201+
return model_validate(AuthMFAAdminDeleteFactorResponse, response.content)
199202

200-
def _validate_uuid(self, id: str) -> None:
203+
def _validate_uuid(self, id: str | None) -> None:
204+
if id is None:
205+
raise ValueError("Invalid id, id cannot be none")
201206
if not is_valid_uuid(id):
202207
raise ValueError(f"Invalid id, '{id}' is not a valid uuid")

0 commit comments

Comments
 (0)