Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 6765f07

Browse files
authored
Merge pull request #293 from supabase-community/or/pydantic-v1-v2-support
Support for pydantic v1 & v2
2 parents c2ed950 + 79cd743 commit 6765f07

File tree

14 files changed

+140
-63
lines changed

14 files changed

+140
-63
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ jobs:
2525
run: make run_tests
2626
- name: Upload Coverage
2727
uses: codecov/codecov-action@v3
28+
- name: Run Tests with pydantic v1
29+
run: |
30+
pip install pydantic==1.10.12
31+
make tests_only
2832
2933
publish:
3034
needs: test

gotrue/_async/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Any, Dict, List, Optional, Union
44

5-
from pydantic import TypeAdapter
5+
from pydantic import parse_obj_as
66

77
from ..exceptions import APIError
88
from ..helpers import check_response, encode_uri_component
@@ -94,7 +94,7 @@ async def list_users(self) -> List[User]:
9494
raise APIError("No users found in response", 400)
9595
if not isinstance(users, list):
9696
raise APIError("Expected a list of users", 400)
97-
return TypeAdapter(List[User]).validate_python(users)
97+
return parse_obj_as(List[User], users)
9898

9999
async def sign_up_with_email(
100100
self,

gotrue/_async/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY
1212
from ..exceptions import APIError
13+
from ..helpers import model_dump, model_validate
1314
from ..types import (
1415
AuthChangeEvent,
1516
CookieOptions,
@@ -560,7 +561,7 @@ async def _recover_common(self) -> Optional[Tuple[Session, int, int]]:
560561
and session_raw
561562
and isinstance(session_raw, dict)
562563
):
563-
session = Session.model_validate(session_raw)
564+
session = model_validate(Session, session_raw)
564565
expires_at = int(expires_at_raw)
565566
time_now = round(time())
566567
return session, expires_at, time_now
@@ -628,7 +629,7 @@ async def _save_session(self, *, session: Session) -> None:
628629
await self._persist_session(session=session)
629630

630631
async def _persist_session(self, *, session: Session) -> None:
631-
data = {"session": session.model_dump(), "expires_at": session.expires_at}
632+
data = {"session": model_dump(session), "expires_at": session.expires_at}
632633
await self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str))
633634

634635
async def _remove_session(self) -> None:

gotrue/_async/gotrue_admin_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

3+
from functools import partial
34
from typing import Dict, List, Union
45

5-
from ..helpers import parse_link_response, parse_user_response
6+
from ..helpers import model_validate, parse_link_response, parse_user_response
67
from ..http_clients import AsyncClient
78
from ..types import (
89
AdminUserAttributes,
@@ -109,7 +110,7 @@ async def list_users(self) -> List[User]:
109110
return await self._request(
110111
"GET",
111112
"admin/users",
112-
xform=lambda data: [User.model_validate(user) for user in data["users"]]
113+
xform=lambda data: [model_validate(User, user) for user in data["users"]]
113114
if "users" in data
114115
else [],
115116
)
@@ -161,7 +162,7 @@ async def _list_factors(
161162
return await self._request(
162163
"GET",
163164
f"admin/users/{params.get('user_id')}/factors",
164-
xform=AuthMFAAdminListFactorsResponse.model_validate,
165+
xform=partial(model_validate, AuthMFAAdminListFactorsResponse),
165166
)
166167

167168
async def _delete_factor(
@@ -171,5 +172,5 @@ async def _delete_factor(
171172
return await self._request(
172173
"DELETE",
173174
f"admin/users/{params.get('user_id')}/factors/{params.get('factor_id')}",
174-
xform=AuthMFAAdminDeleteFactorResponse.model_validate,
175+
xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse),
175176
)

gotrue/_async/gotrue_base_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import BaseModel
77
from typing_extensions import Literal, Self
88

9-
from ..helpers import handle_exception
9+
from ..helpers import handle_exception, model_dump
1010
from ..http_clients import AsyncClient
1111

1212
T = TypeVar("T")
@@ -108,7 +108,7 @@ async def _request(
108108
url,
109109
headers=headers,
110110
params=query,
111-
json=body.model_dump() if isinstance(body, BaseModel) else body,
111+
json=model_dump(body) if isinstance(body, BaseModel) else body,
112112
)
113113
response.raise_for_status()
114114
result = response if no_resolve_json else response.json()

gotrue/_async/gotrue_client.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from functools import partial
34
from json import loads
45
from time import time
56
from typing import Callable, Dict, List, Tuple, Union
@@ -20,7 +21,14 @@
2021
AuthRetryableError,
2122
AuthSessionMissingError,
2223
)
23-
from ..helpers import decode_jwt_payload, parse_auth_response, parse_user_response
24+
from ..helpers import (
25+
decode_jwt_payload,
26+
model_dump,
27+
model_dump_json,
28+
model_validate,
29+
parse_auth_response,
30+
parse_user_response,
31+
)
2432
from ..http_clients import AsyncClient
2533
from ..timer import Timer
2634
from ..types import (
@@ -531,7 +539,7 @@ async def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse:
531539
"factors",
532540
body=params,
533541
jwt=session.access_token,
534-
xform=AuthMFAEnrollResponse.model_validate,
542+
xform=partial(model_validate, AuthMFAEnrollResponse),
535543
)
536544
if response.totp.qr_code:
537545
response.totp.qr_code = f"data:image/svg+xml;utf-8,{response.totp.qr_code}"
@@ -545,7 +553,7 @@ async def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespon
545553
"POST",
546554
f"factors/{params.get('factor_id')}/challenge",
547555
jwt=session.access_token,
548-
xform=AuthMFAChallengeResponse.model_validate,
556+
xform=partial(model_validate, AuthMFAChallengeResponse),
549557
)
550558

551559
async def _challenge_and_verify(
@@ -574,9 +582,9 @@ async def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse:
574582
f"factors/{params.get('factor_id')}/verify",
575583
body=params,
576584
jwt=session.access_token,
577-
xform=AuthMFAVerifyResponse.model_validate,
585+
xform=partial(model_validate, AuthMFAVerifyResponse),
578586
)
579-
session = Session.model_validate(response.model_dump())
587+
session = model_validate(Session, model_dump(response))
580588
await self._save_session(session)
581589
self._notify_all_subscribers("MFA_CHALLENGE_VERIFIED", session)
582590
return response
@@ -589,7 +597,7 @@ async def _unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse:
589597
"DELETE",
590598
f"factors/{params.get('factor_id')}",
591599
jwt=session.access_token,
592-
xform=AuthMFAUnenrollResponse.model_validate,
600+
xform=partial(AuthMFAUnenrollResponse, model_validate),
593601
)
594602

595603
async def _list_factors(self) -> AuthMFAListFactorsResponse:
@@ -751,7 +759,7 @@ async def _save_session(self, session: Session) -> None:
751759
value = (expire_in - refresh_duration_before_expires) * 1000
752760
await self._start_auto_refresh_token(value)
753761
if self._persist_session and session.expires_at:
754-
await self._storage.set_item(self._storage_key, session.model_dump_json())
762+
await self._storage.set_item(self._storage_key, model_dump_json(session))
755763

756764
async def _start_auto_refresh_token(self, value: float) -> None:
757765
if self._refresh_token_timer:
@@ -808,7 +816,7 @@ def _get_valid_session(
808816
except ValueError:
809817
return None
810818
try:
811-
return Session.model_validate(data)
819+
return model_validate(Session, data)
812820
except Exception:
813821
return None
814822

gotrue/_sync/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Any, Dict, List, Optional, Union
44

5-
from pydantic import TypeAdapter
5+
from pydantic import parse_obj_as
66

77
from ..exceptions import APIError
88
from ..helpers import check_response, encode_uri_component
@@ -94,7 +94,7 @@ def list_users(self) -> List[User]:
9494
raise APIError("No users found in response", 400)
9595
if not isinstance(users, list):
9696
raise APIError("Expected a list of users", 400)
97-
return TypeAdapter(List[User]).validate_python(users)
97+
return parse_obj_as(List[User], users)
9898

9999
def sign_up_with_email(
100100
self,

gotrue/_sync/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY
1212
from ..exceptions import APIError
13+
from ..helpers import model_dump, model_validate
1314
from ..types import (
1415
AuthChangeEvent,
1516
CookieOptions,
@@ -556,7 +557,7 @@ def _recover_common(self) -> Optional[Tuple[Session, int, int]]:
556557
and session_raw
557558
and isinstance(session_raw, dict)
558559
):
559-
session = Session.model_validate(session_raw)
560+
session = model_validate(Session, session_raw)
560561
expires_at = int(expires_at_raw)
561562
time_now = round(time())
562563
return session, expires_at, time_now
@@ -620,7 +621,7 @@ def _save_session(self, *, session: Session) -> None:
620621
self._persist_session(session=session)
621622

622623
def _persist_session(self, *, session: Session) -> None:
623-
data = {"session": session.model_dump(), "expires_at": session.expires_at}
624+
data = {"session": model_dump(session), "expires_at": session.expires_at}
624625
self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str))
625626

626627
def _remove_session(self) -> None:

gotrue/_sync/gotrue_admin_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

3+
from functools import partial
34
from typing import Dict, List, Union
45

5-
from ..helpers import parse_link_response, parse_user_response
6+
from ..helpers import model_validate, parse_link_response, parse_user_response
67
from ..http_clients import SyncClient
78
from ..types import (
89
AdminUserAttributes,
@@ -109,7 +110,7 @@ def list_users(self) -> List[User]:
109110
return self._request(
110111
"GET",
111112
"admin/users",
112-
xform=lambda data: [User.model_validate(user) for user in data["users"]]
113+
xform=lambda data: [model_validate(User, user) for user in data["users"]]
113114
if "users" in data
114115
else [],
115116
)
@@ -161,7 +162,7 @@ def _list_factors(
161162
return self._request(
162163
"GET",
163164
f"admin/users/{params.get('user_id')}/factors",
164-
xform=AuthMFAAdminListFactorsResponse.model_validate,
165+
xform=partial(model_validate, AuthMFAAdminListFactorsResponse),
165166
)
166167

167168
def _delete_factor(
@@ -171,5 +172,5 @@ def _delete_factor(
171172
return self._request(
172173
"DELETE",
173174
f"admin/users/{params.get('user_id')}/factors/{params.get('factor_id')}",
174-
xform=AuthMFAAdminDeleteFactorResponse.model_validate,
175+
xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse),
175176
)

gotrue/_sync/gotrue_base_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import BaseModel
77
from typing_extensions import Literal, Self
88

9-
from ..helpers import handle_exception
9+
from ..helpers import handle_exception, model_dump
1010
from ..http_clients import SyncClient
1111

1212
T = TypeVar("T")
@@ -108,7 +108,7 @@ def _request(
108108
url,
109109
headers=headers,
110110
params=query,
111-
json=body.model_dump() if isinstance(body, BaseModel) else body,
111+
json=model_dump(body) if isinstance(body, BaseModel) else body,
112112
)
113113
response.raise_for_status()
114114
result = response if no_resolve_json else response.json()

0 commit comments

Comments
 (0)