22
33from __future__ import annotations
44
5- from typing import Protocol
5+ from collections .abc import Mapping
6+ from typing import Protocol , cast
67
7- from ._transport import Transport , invoke , response_payload
8+ from ._transport import AuthRefreshTransport , Transport , invoke , response_payload
9+ from .errors import AuthenticationError , SessionChangedError
810from .models import Session
911
1012_INCOMPLETE_SESSION = "Expected a complete Session"
13+ _NO_ACTIVE_SESSION = "No active session"
1114
1215
1316def _is_non_empty_string (value : object ) -> bool :
@@ -35,6 +38,25 @@ def _copy_complete_session(session: object) -> Session:
3538 )
3639
3740
41+ def _session_from_payload (payload : object ) -> Session :
42+ values : Mapping [object , object ] = (
43+ cast ("Mapping[object, object]" , payload ) if isinstance (payload , Mapping ) else {}
44+ )
45+ raw_user = values .get ("user" )
46+ user : Mapping [object , object ] = (
47+ cast ("Mapping[object, object]" , raw_user )
48+ if isinstance (raw_user , Mapping )
49+ else {}
50+ )
51+ return _copy_complete_session (
52+ Session (
53+ access_token = cast ("str" , values .get ("access_token" )),
54+ refresh_token = cast ("str" , values .get ("refresh_token" )),
55+ user_id = cast ("str" , user .get ("id" )),
56+ )
57+ )
58+
59+
3860class AuthContext (Protocol ):
3961 """Client capabilities required by the authentication facade."""
4062
@@ -49,6 +71,12 @@ def _anon_token(self) -> str: ...
4971
5072 def _set_session (self , session : Session ) -> None : ...
5173
74+ def _capture_session (self ) -> tuple [int , Session | None ]: ...
75+
76+ def _set_session_if_current (self , session : Session , generation : int ) -> bool : ...
77+
78+ def _clear_session_if_current (self , generation : int ) -> bool : ...
79+
5280
5381class Auth :
5482 """Authenticate users and update the client session."""
@@ -76,10 +104,28 @@ def sign_in(self, *, email: str, password: str) -> Session:
76104 password = password ,
77105 )
78106 payload = response_payload (response , 200 )
79- session = Session (
80- access_token = payload ["access_token" ],
81- refresh_token = payload ["refresh_token" ],
82- user_id = payload ["user" ]["id" ],
83- )
107+ session = _session_from_payload (payload )
84108 self ._client ._set_session (session )
85109 return session
110+
111+ def refresh_session (self ) -> Session :
112+ """Refresh and replace the current session."""
113+ generation , current = self ._client ._capture_session ()
114+ if current is None :
115+ raise AuthenticationError (_NO_ACTIVE_SESSION )
116+
117+ transport = cast ("AuthRefreshTransport" , self ._client ._transport )
118+ response = invoke (
119+ transport .auth_refresh ,
120+ authorization = self ._client ._anon_token (),
121+ refresh_token = current .refresh_token ,
122+ )
123+ try :
124+ payload = response_payload (response , 200 )
125+ except AuthenticationError :
126+ self ._client ._clear_session_if_current (generation )
127+ raise
128+ refreshed = _session_from_payload (payload )
129+ if not self ._client ._set_session_if_current (refreshed , generation ):
130+ raise SessionChangedError
131+ return refreshed
0 commit comments