A request can wait to be served for longer than the access token it was sent with lives, and comes back refused for a token that was valid when it left. Uploads that take minutes make that wait ordinary. - tell an expired token apart from any other refusal by the code the API returns, and retry only that one, once - prefer a token another process left behind over minting a second: it saves a round trip, and a rotating provider would retire one that is still in use. Assign it rather than read it, since the setter is what rewrites the session header the retry will carry - settle the token before the request so the one compared afterwards is the one that was actually sent - report an overloaded server when the second attempt is refused too, and let API errors out of the upload path with their own type
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
from typing import Final
|
|
|
|
import requests
|
|
from pydantic import TypeAdapter
|
|
from pydantic.dataclasses import dataclass
|
|
|
|
from mdrsclient.api.base import BaseApi
|
|
from mdrsclient.exceptions import UnauthorizedException
|
|
from mdrsclient.models import Token, User
|
|
|
|
# (connect, read) seconds for the token refresh. Uploads are served by a separate
|
|
# instance, so this one is not queued behind them and has no reason to be slow.
|
|
TOKEN_REFRESH_TIMEOUT: Final[tuple[float, float]] = (5.0, 30.0)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UsersCurrentResponseLaboratory:
|
|
id: int
|
|
name: str
|
|
role: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UsersApiCurrentResponse:
|
|
id: int
|
|
username: str
|
|
first_name: str
|
|
last_name: str
|
|
email: str
|
|
orcid_id: str
|
|
laboratories: list[UsersCurrentResponseLaboratory]
|
|
is_staff: bool
|
|
is_active: bool
|
|
is_superuser: bool
|
|
is_reviewer: bool
|
|
last_login: str # ISO8601
|
|
date_joined: str # ISO8601
|
|
|
|
|
|
class UsersApi(BaseApi):
|
|
ENTRYPOINT: Final[str] = "v3/users/"
|
|
|
|
def current(self) -> User:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "current/"
|
|
response = self.connection.get(url)
|
|
self._raise_response_error(response)
|
|
obj = TypeAdapter(UsersApiCurrentResponse).validate_python(response.json())
|
|
laboratory_ids = list(map(lambda x: x.id, obj.laboratories))
|
|
user = User(id=obj.id, username=obj.username, laboratory_ids=laboratory_ids, is_reviewer=obj.is_reviewer)
|
|
return user
|
|
|
|
def token(self, username: str, password: str) -> Token:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "token/"
|
|
data: dict[str, str | int] = {"username": username, "password": password}
|
|
response = self.connection.post(url, data=data)
|
|
if response.status_code == requests.codes.unauthorized:
|
|
raise UnauthorizedException("Invalid username or password.")
|
|
self._raise_response_error(response)
|
|
token = TypeAdapter(Token).validate_python(response.json())
|
|
return token
|
|
|
|
def tokenRefresh(self, token: Token) -> Token:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "token/refresh/"
|
|
data: dict[str, str | int] = {"refresh": token.refresh}
|
|
# Bounded on its own: the caller holds a lock that spans processes while this runs,
|
|
# so a provider that accepts the connection and then goes quiet would otherwise
|
|
# stall every other request on this machine rather than just this one.
|
|
response = self.connection.post(url, data=data, timeout=TOKEN_REFRESH_TIMEOUT)
|
|
if response.status_code == requests.codes.unauthorized:
|
|
raise UnauthorizedException("Token is invalid or expired.")
|
|
self._raise_response_error(response)
|
|
token = TypeAdapter(Token).validate_python(response.json())
|
|
return token
|