Source code for minim.api.qobuz._core

from __future__ import annotations
import base64
from datetime import datetime
import getpass
import hashlib
import json
import os
import re
from typing import TYPE_CHECKING
from urllib.parse import urlencode, urlparse
import warnings

import httpx

from ... import FOUND
from ..._utility import join_values
from .._shared import OAuthAPIClient, TokenDatabase
from ._private_api.albums import PrivateAlbumsAPI
from ._private_api.artists import PrivateArtistsAPI
from ._private_api.catalog import PrivateCatalogAPI
from ._private_api.dynamic import PrivateDynamicAPI
from ._private_api.favorites import PrivateFavoritesAPI
from ._private_api.labels import PrivateLabelsAPI
from ._private_api.genres import PrivateGenresAPI
from ._private_api.playlists import PrivatePlaylistsAPI
from ._private_api.purchases import PrivatePurchasesAPI
from ._private_api.search import PrivateSearchAPI
from ._private_api.tracks import PrivateTracksAPI
from ._private_api.users import PrivateUsersAPI

if FOUND["playwright"]:
    from playwright.sync_api import sync_playwright

if TYPE_CHECKING:
    from typing import Any

    from ..._types import Collection


[docs] class PrivateQobuzAPIClient(OAuthAPIClient): """ Private Qobuz API client. .. attention:: As the private Qobuz API is not designed to be publicly accessible, this client may break without warning if Qobuz makes internal changes or be disabled and removed at any time to ensure compliance with the `Qobuz API Terms of Use <https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf>`_. """ _APP_RE = re.compile(r"/resources/.*/bundle.js") _ID_KEY_RE = re.compile( r'production:\{api:\{appId:"([^"]+)",appSecret.*?privateKey:\s*"([^"]+)"' ) _SEED_RE = re.compile( r'[a-z]\.initialSeed\("([^"]+)",window\.utimezone\.([^)]+)\)' ) _ALLOWED_AUTH_FLOWS = _AUTH_FLOWS = { None: "unauthenticated client", "ext_auth_code": "Qobuz authorization code flow", "password": "Qobuz Web Player login flow", } _ENV_VAR_PREFIX = "PRIVATE_QOBUZ_API" _OPTIONAL_AUTH = True _PROVIDER = "Qobuz" _REDIRECT_FLOWS = {"ext_auth_code"} _QUAL_NAME = f"minim.api.{_PROVIDER.lower()}.{__qualname__}" _VERSION = "1.17" AUTH_URL = "https://www.qobuz.com/signin/oauth" BASE_URL = "https://www.qobuz.com/api.json/0.2" #: Token endpoint. TOKEN_URL = f"{BASE_URL}/oauth/callback" #: Web Player URL. WEB_PLAYER_URL = "https://play.qobuz.com" __slots__ = ( "_app_id", "_app_secret", "_auth_key", "_credential_handler", "_token_extras", "albums", "artists", "catalog", "dynamic", "favorites", "labels", "genres", "playlists", "purchases", "search", "tracks", "users", ) def __init__( self, *, auth_flow: str | None = None, app_id: str | None = None, app_secret: str | None = None, auth_key: str | None = None, user_identifier: str | None = None, redirect_uri: str | None = None, user_auth_token: str | None = None, credential_handler: str | None = None, redirect_handler: str | None = None, open_browser: bool = False, enable_cache: bool = True, store_tokens: bool = True, user_agent: str | None = None, **kwargs: dict[str, Any], ) -> None: """ Parameters ---------- auth_flow : str or None; keyword-only; optional Authorization flow. **Valid values**: * :code:`None` – No authentication. * :code:`"ext_auth_code"` – Qobuz authorization code flow. * :code:`"password"` – Qobuz Web Player login flow. app_id : str; keyword-only; optional Application ID. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_APP_ID` or from the local token storage if available, or retrieved from the Qobuz Web Player login page otherwise. app_secret : str; keyword-only; optional Application secret. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_APP_SECRET` or from the local token storage if available, or retrieved from the Qobuz Web Player login page otherwise. auth_key : str; keyword-only; optional Authorization key used in the Authorization Code flow. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_AUTH_KEY` if available, or retrieved from the Qobuz Web Player login page otherwise. user_identifier : str; keyword-only; optional Identifier for the user account. Used when :code:`store_tokens=True` to distinguish between multiple accounts for the same client ID and authorization flow. If specified, it is used with the client ID and authorization flow to locate a matching stored token. If none is found, a new token is obtained and stored under this identifier. If not specified, the most recently accessed token for the client ID and authorization flow is used. If none exists, a new token is obtained and stored using a user identifier (e.g., user ID) acquired from a successful authorization. Prefixing the identifier with a tilde (:code:`~`) bypasses token retrieval, forces reauthorization, and stores the new token under the suffix. redirect_uri : str; keyword-only; optional Redirect URI. Required for the Authorization Code flow. user_auth_token : str; keyword-only; optional User authentication token. If provided, the authorization process is bypassed. credential_handler : str; keyword-only; optional Backend for handling user credentials during the Qobuz Web Player login flow. If not specified, the client first looks for credentials in `kwargs` and falls back to prompting for them in an echo-free terminal. **Valid values**: * :code:`"kwargs"` – User credentials (email or username and password or its MD5 hash) provided directly via the `username` and `password` keyword arguments, respectively. * :code:`"getpass"` – Prompt for credentials in an echo-free terminal. * :code:`"playwright"` – Open the Qobuz Web Player login page in a Playwright Firefox browser. redirect_handler : str or None; keyword-only; optional Backend for handling redirects during the Authorization Code flow. Redirect handling is only available for hosts :code:`localhost`, :code:`127.0.0.1`, or :code:`::1`. **Valid values**: * :code:`None` – Show authorization URL in and have the user manually paste the redirect URL into the terminal. * :code:`"http.server"` – Run a HTTP server to intercept the redirect after user authorization in any local browser. * :code:`"playwright"` – Use a Playwright Firefox browser to complete the user authorization. open_browser : bool; keyword-only; default: :code:`False` Whether to automatically open the authorization URL in the default web browser for the Authorization Code flow. If :code:`False`, the URL is printed to the terminal. enable_cache : bool; keyword-only; default: :code:`True` Whether to enable an in-memory time-to-live (TTL) cache with a least recently used (LRU) eviction policy for this client. If :code:`True`, responses from semi-static endpoints are cached for one minute to one day, depending on their expected update frequency. .. seealso:: :meth:`clear_cache` – Clear specific or all cache entries for this client. store_tokens : bool; keyword-only; default: :code:`True` Whether to enable the local token storage for this client. If :code:`True`, existing user authentication tokens are retrieved when found in local storage, and newly acquired tokens and their metadata are stored for future retrieval. If :code:`False`, the client neither retrieves nor stores tokens. .. seealso:: :meth:`get_tokens` – Retrieve specific or all stored tokens for this client. :meth:`remove_tokens` – Remove specific or all stored tokens for this client. user_agent : str; keyword-only; optional :code:`User-Agent` value to include in the headers of HTTP requests. **kwargs : dict[str, Any] Keyword arguments to pass to :meth:`~minim.api.qobuz.PrivateUsersAPI.login`. """ super().__init__(enable_cache=enable_cache, user_agent=user_agent) # Initialize subclasses for endpoint groups #: Albums API endpoints for the private Qobuz API. self.albums: PrivateAlbumsAPI = PrivateAlbumsAPI(self) #: Artists API endpoints for the private Qobuz API. self.artists: PrivateArtistsAPI = PrivateArtistsAPI(self) #: Catalog API endpoints for the private Qobuz API. self.catalog: PrivateCatalogAPI = PrivateCatalogAPI(self) #: Dynamic Tracks API endpoints for the private Qobuz API. self.dynamic: PrivateDynamicAPI = PrivateDynamicAPI(self) #: Favorites API endpoints for the private Qobuz API. self.favorites: PrivateFavoritesAPI = PrivateFavoritesAPI(self) #: Labels API endpoints for the private Qobuz API. self.labels: PrivateLabelsAPI = PrivateLabelsAPI(self) #: Genres API endpoints for the private Qobuz API. self.genres: PrivateGenresAPI = PrivateGenresAPI(self) #: Playlists API endpoints for the private Qobuz API. self.playlists: PrivatePlaylistsAPI = PrivatePlaylistsAPI(self) #: Purchases API endpoints for the private Qobuz API. self.purchases: PrivatePurchasesAPI = PrivatePurchasesAPI(self) #: Search-related endpoints for the private Qobuz API. self.search: PrivateSearchAPI = PrivateSearchAPI(self) #: Tracks API endpoints for the private Qobuz API. self.tracks: PrivateTracksAPI = PrivateTracksAPI(self) #: Users API endpoints for the private Qobuz API. self.users: PrivateUsersAPI = PrivateUsersAPI(self) # If an app ID is not provided, try to retrieve it and its # corresponding app secret from environment variables if app_id is None or app_secret is None: app_id = os.environ.get(f"{self._ENV_VAR_PREFIX}_APP_ID") app_secret = os.environ.get(f"{self._ENV_VAR_PREFIX}_APP_SECRET") # If the authorization key for the Authorization Code Flow is # not provided, try to retrieve it from environment variables if auth_flow == "auth_code" and auth_key is None: auth_key = os.environ.get(f"{self._ENV_VAR_PREFIX}_AUTH_KEY") # If the app ID or authorization key is still missing, get it # from Qobuz if app_id is None or auth_flow == "auth_code" and auth_key is None: app_id, auth_key, app_secret = self._resolve_app_credentials() if auth_flow is not None: if user_identifier and user_identifier[0] == "~": user_identifier = user_identifier[1:] elif store_tokens and ( account := TokenDatabase._get_token( self.__class__.__name__, auth_flow=auth_flow, client_id=app_id, user_identifier=user_identifier, ) ): # If a user authentication token is not provided, try # to retrieve it from local token storage user_auth_token = account["access_token"] app_secret = account["client_secret"] redirect_uri = account["redirect_uri"] self._token_extras = ( json.loads(token_extras) if isinstance(token_extras := account["extras"], str) else token_extras ) self.set_auth_flow( auth_flow, app_id=app_id, app_secret=app_secret, auth_key=auth_key, user_identifier=user_identifier, redirect_uri=redirect_uri, credential_handler=credential_handler, redirect_handler=redirect_handler, open_browser=open_browser, store_tokens=store_tokens, authenticate=False, ) if auth_flow is None: self._determine_app_secret() elif user_auth_token: self.set_user_auth_token(user_auth_token) else: self._obtain_user_auth_token(**kwargs)
[docs] @classmethod def get_tokens( cls, *, auth_flows: str | Collection[str] | None = None, app_ids: str | Collection[str] | None = None, user_identifiers: str | Collection[str] | None = None, ) -> list[dict[str, Any]] | None: """ Retrieve specific or all user authentication tokens and their metadata for this client from local storage. Parameters ---------- auth_flows : str or Collection[str]; keyword-only; optional Authorization flows. app_ids : str or Collection[str]; keyword-only; optional Application IDs. user_identifiers : str or Collection[str]; keyword-only; \ optional Identifiers for the user accounts. """ TokenDatabase.get_tokens( client_names=cls.__name__, auth_flows=auth_flows, client_ids=app_ids, user_identifiers=user_identifiers, )
[docs] @classmethod def remove_tokens( cls, *, auth_flows: str | Collection[str] | None = None, app_ids: str | Collection[str] | None = None, user_identifiers: str | Collection[str] | None = None, ) -> None: """ Remove specific or all user authentication tokens and their metadata for this client from local storage. .. warning:: If none of `auth_flows`, `app_ids`, or `user_identifiers` are provided, all tokens for this client will be removed from local storage. Parameters ---------- auth_flows : str or Collection[str]; keyword-only; optional Authorization flows. app_ids : str or Collection[str]; keyword-only; optional Application IDs. user_identifiers : str or Collection[str]; keyword-only; \ optional Identifiers for the user accounts. """ TokenDatabase.remove_tokens( client_names=cls.__name__, auth_flows=auth_flows, client_ids=app_ids, user_identifiers=user_identifiers, )
@classmethod def _resolve_app_credentials(cls) -> tuple[str, list[str], str]: """ Resolve the application ID and secret using the Qobuz Web Player login page. Returns ------- app_id : str Application ID. auth_key : str Authorization key used in the Authorization Code flow. app_secrets : list[str] Possible application secrets. """ with httpx.Client( base_url=cls.WEB_PLAYER_URL, follow_redirects=True ) as client: m = cls._APP_RE.search(client.get("login").text) if m is None: raise RuntimeError("'bundle.js' was not found.") # /resources/8.1.0-b019/bundle.js bundle = client.get(m.group(0)).text return ( *cls._ID_KEY_RE.search(bundle).groups(), [ base64.b64decode( "".join((secret, *match.groups()))[:-44] ).decode(encoding="utf-8") for secret, match in ( ( secret, re.search( f'(?:{city.capitalize()}",info:")(.*?)(?:",extras:")' '(.*?)(?:"},{offset)', bundle, ), ) for secret, city in cls._SEED_RE.findall(bundle) ) if match ], ) def _determine_app_secret(self) -> None: """ Determine the working application secret from the possible values. """ if isinstance(self._app_secret, list): for app_secret in self._app_secret: try: self._app_secret = app_secret self.tracks.get_track_media_info(344521217, format_id=5) break except RuntimeError: continue else: raise RuntimeError( "No valid application secret was found in 'bundle.js'." ) def _get_ext_auth_code(self) -> str: """ Get the authorization code for the Qobuz authorization code flow. Returns ------- auth_code : str Authorization code. """ params = { "ext_app_id": self._app_id, "redirect_url": self._redirect_uri, } auth_code = self._handle_redirect( f"{self.AUTH_URL}?{urlencode(params)}", url_component="query" ).get("code_autorisation") if not auth_code: raise RuntimeError("Authorization failed.") return auth_code def _login( self, username: str | None = None, password: str | None = None, **kwargs, ) -> dict[str, Any]: """ Perform the Qobuz Web Player login flow. Parameters ---------- username : str; optional Email or username. password : str; optional Password or its MD5 hash. **kwargs : dict[str, Any] Keyword arguments to pass to :meth:`~minim.api.qobuz.PrivateUsersAPI.login`. Returns ------- token : dict[str, Any] User authentication token and profile information. """ if self._credential_handler == "playwright": if not FOUND["playwright"]: raise RuntimeError( "The Qobuz Web Player login flow uses the " "`playwright` library, but it could not be found " "or imported." ) with sync_playwright() as playwright: browser = playwright.firefox.launch(headless=False) context = browser.new_context( color_scheme="dark", locale="en-US", timezone_id="America/Los_Angeles", **playwright.devices["Desktop Firefox HiDPI"], ) page = context.new_page() with page.expect_request( f"{self.BASE_URL}/oauth/callback*", timeout=0 ) as callback: page.goto(f"{self.WEB_PLAYER_URL}/login") resp_json = callback.value.response().json() context.close() browser.close() return resp_json if self._credential_handler in {None, "kwargs"}: if username is None or password is None: if self._credential_handler == "kwargs": raise ValueError( "`username` and `password` must be provided for " "the Qobuz Web Player login flow." ) else: return self.users.login(username, password, **kwargs) # self._credential_handler in {None, "getpass"} print( f"To grant Minim access to {self._PROVIDER} data " "and features, log in below:\n" ) return self.users.login( input("Username: "), hashlib.md5( getpass.getpass().encode(encoding="utf-8") ).hexdigest(), **kwargs, ) def _obtain_access_token(self): pass # Implemented as _obtain_user_auth_token() def _obtain_user_auth_token( self, auth_flow: str | None = None, **kwargs: dict[str, Any] ) -> None: """ Get and set a new user authentication token via the provided or current authorization flow. Parameters ---------- auth_flow : str; optional Authorization flow. If not provided, the current authorization flow in :attr:`_auth_flow` is used. **Valid values**: * :code:`None` – No authentication. * :code:`"password"` – Qobuz Web Player login flow. **kwargs : dict[str, Any] Keyword arguments to pass to :meth:`~minim.api.qobuz.PrivateUsersAPI.login`. """ if not auth_flow: auth_flow = self._auth_flow match auth_flow: case None: self.set_user_auth_token(None) return case "ext_auth_code": resp_json = self._client.get( self.TOKEN_URL, params={ "code": self._get_ext_auth_code(), "private_key": self._auth_key, }, ).json() case "password": resp_json = self._login(**kwargs) user_auth_token = resp_json.pop( "token" if "token" in resp_json else "user_auth_token" ) self.set_user_auth_token(user_auth_token) self._token_extras = resp_json if not self._user_identifier and self._auth_flow is not None: self._user_identifier = self._resolve_user_identifier() # Figure out the working application secret from the possible # values self._determine_app_secret() if self._store_tokens: TokenDatabase.add_token( self.__class__.__name__, auth_flow=self._auth_flow, client_id=self._app_id, client_secret=self._app_secret, user_identifier=self._user_identifier, redirect_uri=self._redirect_uri, token_type=None, access_token=user_auth_token, refresh_token=None, expires_at=None, extras=resp_json, ) def _request( self, method: str, endpoint: str, /, *, signed: bool = False, sig_params: dict[str, Any] | None = None, **kwargs: dict[str, Any], ) -> "httpx.Response": """ Make an HTTP request to a private Qobuz API endpoint. Parameters ---------- method : str; positional-only HTTP method. endpoint : str; positional-only Private Qobuz API endpoint. signed : bool; keyword-only; default: :code:`False` Whether to sign the request. sig_params : dict[str, Any]; keyword-only; optional Query parameters to include in the signature. **kwargs : dict[str, Any] Keyword parameters to pass to :meth:`httpx.Client.request`. Returns ------- response : httpx.Response HTTP response. """ if signed: timestamp = datetime.now().timestamp() signature = "".join( f"{k}{str(v).lower() if isinstance(v, bool) else v}" for k, v in sorted(sig_params.items()) ) if "params" not in kwargs: kwargs["params"] = {} kwargs["params"] |= { "request_ts": timestamp, "request_sig": hashlib.md5( ( f"{endpoint.replace('/', '')}{signature}" f"{timestamp}{self._app_secret}" ).encode(encoding="utf-8") ).hexdigest(), } resp = self._client.request(method, endpoint, **kwargs) status = resp.status_code if 200 <= status < 300: return resp raise RuntimeError( f"{status} {resp.reason_phrase}{resp.json()['message']}" ) def _require_authentication(self, endpoint_method: str, /) -> None: """ Ensure that the user authentication has been performed for a protected endpoint. Parameters ---------- endpoint_method : str; positional-only Name of the endpoint method. """ if self._auth_flow is None: raise RuntimeError( f"{self._QUAL_NAME}.{endpoint_method}() requires user " "authentication." ) def _resolve_user_identifier(self) -> str: """ Return the Qobuz user ID as the user identifier for the current account. .. note:: Invoking this method may call :meth:`~minim.api.qobuz.UsersAPI.get_me` and make a request to the private Qobuz API. """ return ( self._token_extras.get("user_id") or self._token_extras.get("user", {}).get("id") or self.users.get_me() )
[docs] def set_auth_flow( self, auth_flow: str | None, /, *, app_id: str | None = None, app_secret: str | None = None, auth_key: str | None = None, user_identifier: str | None = None, redirect_uri: str | None = None, credential_handler: str | None = None, redirect_handler: str | None = None, open_browser: bool = False, store_tokens: bool = True, authenticate: bool = True, **kwargs: dict[str, Any], ) -> None: """ Set or update the authorization flow and related parameters. .. warning:: Calling this method replaces all existing values with the provided parameters. Parameters not provided explicitly will be overwritten by their default values. Parameters ---------- auth_flow : str or None; keyword-only Authorization flow. **Valid values**: * :code:`None` – No authentication. * :code:`"ext_auth_code"` – Qobuz authorization code flow. * :code:`"password"` – Qobuz Web Player login flow. app_id : str; keyword-only; optional Application ID. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_APP_ID`. app_secret : str; keyword-only; optional Application secret. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_APP_SECRET`. auth_key : str; keyword-only; optional Authorization key used in the Authorization Code flow. If not provided, it is loaded from the system environment variable :code:`PRIVATE_QOBUZ_API_AUTH_KEY`. user_identifier : str; keyword-only; optional Identifier for the user account. Used when :code:`store_tokens=True` to distinguish between multiple accounts for the same client ID and authorization flow. If specified, it is used with the client ID and authorization flow to locate a matching stored token. If none is found, a new token is obtained and stored under this identifier. If not specified, the most recently accessed token for the client ID and authorization flow is used. If none exists, a new token is obtained and stored using a user identifier (e.g., user ID) acquired from a successful authorization. Prefixing the identifier with a tilde (:code:`~`) bypasses token retrieval, forces reauthorization, and stores the new token under the suffix. redirect_uri : str; keyword-only; optional Redirect URI. Required for the Authorization Code flow. credential_handler : str; keyword-only; optional Backend for handling user credentials during the Qobuz Web Player login flow. If not specified, the client first looks for credentials in `kwargs` and falls back to prompting for them in an echo-free terminal. **Valid values**: * :code:`"kwargs"` – Use credentials provided directly via the `username` and `password` keyword arguments. * :code:`"getpass"` – Prompt for credentials in an echo-free terminal. * :code:`"playwright"` – Open the Qobuz Web Player login page in a Playwright Firefox browser. redirect_handler : str or None; keyword-only; optional Backend for handling redirects during the Authorization Code flow. Redirect handling is only available for hosts :code:`localhost`, :code:`127.0.0.1`, or :code:`::1`. **Valid values**: * :code:`None` – Show authorization URL in and have the user manually paste the redirect URL into the terminal. * :code:`"http.server"` – Run a HTTP server to intercept the redirect after user authorization in any local browser. * :code:`"playwright"` – Use a Playwright Firefox browser to complete the user authorization. open_browser : bool; keyword-only; default: :code:`False` Whether to automatically open the authorization URL in the default web browser for the Authorization Code flow. If :code:`False`, the URL is printed to the terminal. store_tokens : bool; keyword-only; default: :code:`True` Whether to enable the local token storage for this client. If :code:`True`, existing user authentication tokens are retrieved when found in local storage, and newly acquired tokens and their metadata are stored for future retrieval. If :code:`False`, the client neither retrieves nor stores tokens. .. seealso:: :meth:`get_tokens` – Retrieve specific or all stored tokens for this client. :meth:`remove_tokens` – Remove specific or all stored tokens for this client. authenticate : bool; keyword-only; default: :code:`True` Whether to immediately initiate the authorization flow to acquire a user authentication token. .. important:: Unless :meth:`set_user_auth_token` is called immediately after, this should be left as :code:`True` to ensure the client's existing token is compatible with the new authorization flow. **kwargs : dict[str, Any] Keyword arguments to pass to :meth:`~minim.api.qobuz.PrivateUsersAPI.login`. """ if auth_flow == "auth_code" and auth_key is None: auth_key = os.environ.get(f"f{self._ENV_VAR_PREFIX}_AUTH_KEY") if auth_key is None: raise ValueError( "The authorization key must be provided via the " "`auth_key` parameter." ) if app_id is None or app_secret is None: app_id = os.environ.get(f"{self._ENV_VAR_PREFIX}_APP_ID") if app_id is None: raise ValueError( "An application ID must be provided via the `app_id` " "parameter." ) app_secret = os.environ.get(f"{self._ENV_VAR_PREFIX}_APP_SECRET") if app_secret is None: raise ValueError( "An application secret must be provided via the " "`app_secret` parameter." ) if auth_flow not in self._AUTH_FLOWS: raise ValueError( f"Invalid authorization flow {auth_flow!r}. " f"Valid values: {self._join_values(self._AUTH_FLOWS)}." ) self._client.headers["x-app-id"] = self._app_id = app_id self._app_secret = app_secret self._auth_key = auth_key self._credential_handler = credential_handler super().set_auth_flow( auth_flow=auth_flow, user_identifier=user_identifier, redirect_uri=redirect_uri, redirect_handler=redirect_handler, open_browser=open_browser, store_tokens=store_tokens, authenticate=False, ) if authenticate and auth_flow is not None: self._obtain_user_auth_token(**kwargs)
[docs] def set_access_token(self, access_token: str) -> None: """ Set or update the access token. .. note:: For this API client, access tokens are user authentication tokens. .. seealso:: :meth:`set_user_auth_token` – Set or update the user authentication token. Parameters ---------- access_token : str or None; positional-only Access token. """ self.set_user_auth_token(access_token)
[docs] def set_user_auth_token(self, user_auth_token: str | None, /) -> None: """ Set or update the user authentication token. Parameters ---------- user_auth_token : str or None; positional-only User authentication token. """ if user_auth_token is None: if self._auth_flow is not None: raise ValueError( "`user_auth_token` cannot be None when using the " f"{self._AUTH_FLOWS[self._auth_flow]}." ) if "x-user-auth-token" in self._client.headers: del self._client.headers["x-user-auth-token"] return elif isinstance(user_auth_token, str): self._client.headers["x-user-auth-token"] = user_auth_token else: raise TypeError("`user_auth_token` must be a string.")