Skip to content

Request Handler

The api request handler singleton

ApiRequestHandler

The request handler that makes the calls to the spotify api. This class is a singleton.

Source code in async_spotify/api/_api_request_maker.py
class ApiRequestHandler:
    """
    The request handler that makes the calls to the spotify api.
    This class is a singleton.
    """

    def __init__(self, spotify_authorisation_token: SpotifyAuthorisationToken,
                 token_renew_instance: TokenRenewClass,
                 spotify_api_client):
        """
        Create a new ApiRequestHandler class. The api class should be at least once passed to the constructor of this
        class. Otherwise it will not work.

        Args:
            spotify_authorisation_token: The auth token of the api class
            token_renew_instance: An instance of a token renew class
            spotify_api_client: The spotify api client
        """

        self.spotify_authorisation_token: SpotifyAuthorisationToken = spotify_authorisation_token
        self.token_renew_instance: TokenRenewClass = token_renew_instance
        self.__spotify_api_client = spotify_api_client
        self.client_session_list: Optional[Deque[ClientSession]] = deque([])

    async def create_new_client(self, request_timeout: int, request_limit: int) -> None:
        """
        Create a new client

        Args:
            request_timeout: The timout which should be used for making requests
            request_limit: The maximal number of requests per session
        """

        if self.client_session_list:
            await self.close_client()

        client_instance_number: int = math.ceil(request_limit / 500)

        for _ in range(client_instance_number):
            timeout = ClientTimeout(total=request_timeout)
            connector = TCPConnector(limit=request_limit, enable_cleanup_closed=True)
            client_session = ClientSession(connector=connector, timeout=timeout, cookie_jar=DummyCookieJar())

            self.client_session_list.append(client_session)

    async def close_client(self) -> None:
        """
        Close the current client session. You have to create a new one to connect again to spotify.
        This method should always be called before you end your program
        """

        for client in self.client_session_list:
            await client.close()

        self.client_session_list: Deque = deque([])

    async def make_request(self,
                           method: str,
                           url: str,
                           query_params: Optional[dict],
                           auth_token: SpotifyAuthorisationToken,
                           body: dict = None,
                           last_try=False) \
            -> Union[dict, List[bool], None, bool]:
        """
        Make a request to the spotify api

        Args:
            method: The method that should be used (get, post, put, delete)
            url: The url the request is going to
            query_params: URL query params for the request
            auth_token: The auth token (None if the in memory token should be used)
            body: Add a body to the request
            last_try: Check if this is the last try (used if you use a token refresh class)

        Returns: The spotify api response
        """

        if not self.client_session_list:
            message = 'You have to create a new client with create_new_client ' \
                      'before you can make requests to the spotify api.'
            raise SpotifyError(ErrorMessage(message=message).__dict__)

        # Prepare the data for the api request
        url_params, headers, updated_body = self._prepare_request_parameters(auth_token, query_params, body)

        # Round robin so you use a different client for every new request
        self.client_session_list.rotate(1)
        client: ClientSession = self.client_session_list[0]

        # Make the api response
        async with client.request(method, url, params=url_params, headers=headers, data=updated_body) as response:
            response_status = ResponseStatus(response.status)

            # Handle the parsing of the rate limit exceeded response which does not work for some reason
            response_text: str = await response.text()
            response_json: dict = {}
            retry_after: str = response.headers.get('Retry-After', None)

            try:
                response_json: dict = json.loads(response_text)
            except JSONDecodeError:
                pass

        # Expired
        if response_status.code == 401:

            if self.token_renew_instance and not last_try:
                auth_token: SpotifyAuthorisationToken = await self.token_renew_instance(self.__spotify_api_client)

                # Update the auth token if the token should be kept in memory
                if self.spotify_authorisation_token.valid:
                    self.spotify_authorisation_token.access_token = auth_token.access_token
                    self.spotify_authorisation_token.activation_time = auth_token.activation_time
                    self.spotify_authorisation_token.refresh_token = auth_token.refresh_token

                return await self.make_request(method, url, query_params, auth_token, body, last_try=True)
            else:
                raise TokenExpired(response_json)

        # Rate limit exceeded
        if response_status.code == 429:
            try:
                float_val = float(retry_after)
            except ValueError:
                float_val = 0
            raise RateLimitExceeded(message=response_json, retry_after=float_val)

        # Check if the response was a success
        if not response_status.success:
            raise SpotifyAPIError(response_json)

        return response_json

    def _prepare_request_parameters(self, auth_token: SpotifyAuthorisationToken, query_params: dict, body: dict) \
            -> Tuple[List[Tuple[str, str]], dict, str]:
        """
        Prepare the request parameters for the aiohttp request

        Args:
            auth_token: An auth_token
            query_params: URL params for the request
            body: The request body (either string or dict)

        Returns:
            A tuple with the
                url_params
                headers
                body
            in the right format
        """
        url_params: List[Tuple[str, str]] = self._format_params(query_params)
        headers = self._get_headers(auth_token)

        # Check if the body should be a json or an image
        if body and isinstance(body, dict):
            body = json.dumps(body)
        elif body:
            headers['Content-Type'] = 'image/jpeg'

        return url_params, headers, body

    @staticmethod
    def _format_params(query_params: dict) -> List[Tuple[str, str]]:
        """
        Converts the query dict into the aiohttp conform Type

        Args:
            query_params: The query params

        Returns: The aiohttp conform object
        """

        return_params: List[Tuple[str, str]] = []

        for key in list(query_params.keys()):
            if not isinstance(query_params[key], List):
                query_params[key] = [str(query_params[key])]

            return_params.append((key, ",".join([str(i) for i in query_params[key]])))

        return return_params

    def _get_headers(self, auth_token: SpotifyAuthorisationToken) -> dict:
        """
        Build the spotify header used to authenticate the user for the spotify api

        Args:
            auth_token: The spotify auth token

        Returns: The header as json
        """

        if not auth_token:

            if self.spotify_authorisation_token.valid:
                auth_token = self.spotify_authorisation_token
            else:
                message = 'You have to provide a valid auth token or set the option hold_authentication to true and ' \
                          'call the get_auth_token_with_code or refresh_token method at leas once'
                raise SpotifyError(ErrorMessage(message=message).__dict__)

        return {
            'Authorization': f'Bearer {auth_token.access_token}',
            'Content-Type': 'application/json'
        }

__init__(self, spotify_authorisation_token, token_renew_instance, spotify_api_client) special

Create a new ApiRequestHandler class. The api class should be at least once passed to the constructor of this class. Otherwise it will not work.

Parameters:

Name Type Description Default
spotify_authorisation_token SpotifyAuthorisationToken

The auth token of the api class

required
token_renew_instance TokenRenewClass

An instance of a token renew class

required
spotify_api_client

The spotify api client

required
Source code in async_spotify/api/_api_request_maker.py
def __init__(self, spotify_authorisation_token: SpotifyAuthorisationToken,
             token_renew_instance: TokenRenewClass,
             spotify_api_client):
    """
    Create a new ApiRequestHandler class. The api class should be at least once passed to the constructor of this
    class. Otherwise it will not work.

    Args:
        spotify_authorisation_token: The auth token of the api class
        token_renew_instance: An instance of a token renew class
        spotify_api_client: The spotify api client
    """

    self.spotify_authorisation_token: SpotifyAuthorisationToken = spotify_authorisation_token
    self.token_renew_instance: TokenRenewClass = token_renew_instance
    self.__spotify_api_client = spotify_api_client
    self.client_session_list: Optional[Deque[ClientSession]] = deque([])

close_client(self) async

Close the current client session. You have to create a new one to connect again to spotify. This method should always be called before you end your program

Source code in async_spotify/api/_api_request_maker.py
async def close_client(self) -> None:
    """
    Close the current client session. You have to create a new one to connect again to spotify.
    This method should always be called before you end your program
    """

    for client in self.client_session_list:
        await client.close()

    self.client_session_list: Deque = deque([])

create_new_client(self, request_timeout, request_limit) async

Create a new client

Parameters:

Name Type Description Default
request_timeout int

The timout which should be used for making requests

required
request_limit int

The maximal number of requests per session

required
Source code in async_spotify/api/_api_request_maker.py
async def create_new_client(self, request_timeout: int, request_limit: int) -> None:
    """
    Create a new client

    Args:
        request_timeout: The timout which should be used for making requests
        request_limit: The maximal number of requests per session
    """

    if self.client_session_list:
        await self.close_client()

    client_instance_number: int = math.ceil(request_limit / 500)

    for _ in range(client_instance_number):
        timeout = ClientTimeout(total=request_timeout)
        connector = TCPConnector(limit=request_limit, enable_cleanup_closed=True)
        client_session = ClientSession(connector=connector, timeout=timeout, cookie_jar=DummyCookieJar())

        self.client_session_list.append(client_session)

make_request(self, method, url, query_params, auth_token, body=None, last_try=False) async

Make a request to the spotify api

Parameters:

Name Type Description Default
method str

The method that should be used (get, post, put, delete)

required
url str

The url the request is going to

required
query_params Optional[dict]

URL query params for the request

required
auth_token SpotifyAuthorisationToken

The auth token (None if the in memory token should be used)

required
body dict

Add a body to the request

None
last_try

Check if this is the last try (used if you use a token refresh class)

False

Returns: The spotify api response

Source code in async_spotify/api/_api_request_maker.py
async def make_request(self,
                       method: str,
                       url: str,
                       query_params: Optional[dict],
                       auth_token: SpotifyAuthorisationToken,
                       body: dict = None,
                       last_try=False) \
        -> Union[dict, List[bool], None, bool]:
    """
    Make a request to the spotify api

    Args:
        method: The method that should be used (get, post, put, delete)
        url: The url the request is going to
        query_params: URL query params for the request
        auth_token: The auth token (None if the in memory token should be used)
        body: Add a body to the request
        last_try: Check if this is the last try (used if you use a token refresh class)

    Returns: The spotify api response
    """

    if not self.client_session_list:
        message = 'You have to create a new client with create_new_client ' \
                  'before you can make requests to the spotify api.'
        raise SpotifyError(ErrorMessage(message=message).__dict__)

    # Prepare the data for the api request
    url_params, headers, updated_body = self._prepare_request_parameters(auth_token, query_params, body)

    # Round robin so you use a different client for every new request
    self.client_session_list.rotate(1)
    client: ClientSession = self.client_session_list[0]

    # Make the api response
    async with client.request(method, url, params=url_params, headers=headers, data=updated_body) as response:
        response_status = ResponseStatus(response.status)

        # Handle the parsing of the rate limit exceeded response which does not work for some reason
        response_text: str = await response.text()
        response_json: dict = {}
        retry_after: str = response.headers.get('Retry-After', None)

        try:
            response_json: dict = json.loads(response_text)
        except JSONDecodeError:
            pass

    # Expired
    if response_status.code == 401:

        if self.token_renew_instance and not last_try:
            auth_token: SpotifyAuthorisationToken = await self.token_renew_instance(self.__spotify_api_client)

            # Update the auth token if the token should be kept in memory
            if self.spotify_authorisation_token.valid:
                self.spotify_authorisation_token.access_token = auth_token.access_token
                self.spotify_authorisation_token.activation_time = auth_token.activation_time
                self.spotify_authorisation_token.refresh_token = auth_token.refresh_token

            return await self.make_request(method, url, query_params, auth_token, body, last_try=True)
        else:
            raise TokenExpired(response_json)

    # Rate limit exceeded
    if response_status.code == 429:
        try:
            float_val = float(retry_after)
        except ValueError:
            float_val = 0
        raise RateLimitExceeded(message=response_json, retry_after=float_val)

    # Check if the response was a success
    if not response_status.success:
        raise SpotifyAPIError(response_json)

    return response_json

Last update: August 28, 2020