from dataclasses import field 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 UsersCurrentResponseProject: id: int name: str | None bit_position: 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 projects: list[UsersCurrentResponseProject] = field(default_factory=list) 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 project_names(self) -> dict[int, str]: """Names of the user's projects by bit_position, the bit their access level sets.""" url = self.ENTRYPOINT + "current/" response = self.connection.get(url) self._raise_response_error(response) obj = TypeAdapter(UsersApiCurrentResponse).validate_python(response.json()) names: dict[int, list[str]] = {} for project in obj.projects: if project.name: # bit_position is not unique on the server, and a folder at a shared bit # belongs to every project holding it, so all of them are named. names.setdefault(project.bit_position, []).append(project.name) return {bit: ", ".join(sorted(x)) for bit, x in names.items()} 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