mental32 / spotify.py

🌐 API wrapper for Spotify 🎶
https://spotifypy.readthedocs.io/en/latest/
MIT License
150 stars 38 forks source link

Removed `await` in return of `User.from_code()` for FastAPI / Flask support #68

Closed bmeares closed 3 years ago

bmeares commented 3 years ago

Over the weekend, I was mentoring at a hackathon, and a team was trying to use this library. We couldn't figure out how to get the callback from Spotify working, so I poked around the source.

User.from_code() was always returning a coroutine, even when awaiting the function or using import spotify.sync as spotify. Removing await from return await cls.from_token(client, token, refresh_token) seemed to do the trick.

I hope this change doesn't break other functionality. Everything passed when I ran pytest, I suspect User.from_code() might not be covered. I tried writing a new unit test but wasn't sure how to get a valid code without spinning up a web server.

Admittedly I'm not as comfortable with async as I'd like to be, but now await spotify.User.from_code(...) seems to work. Below is an example implementation in FastAPI:

from fastapi import FastAPI, Response, Cookie
import spotify, random, string
from starlette.responses import RedirectResponse
from typing import *
from base64 import b64encode

async def from_code(
        cls, client: "spotify.Client", code: str, *, redirect_uri: str,
    ):
        """Create a :class:`User` object from an authorization code.

        Parameters
        ----------
        client : :class:`spotify.Client`
            The spotify client to associate the user with.
        code : :class:`str`
            The authorization code to use to further authenticate the user.
        redirect_uri : :class:`str`
            The rediriect URI to use in tandem with the authorization code.
        """
        route = ("POST", "https://accounts.spotify.com/api/token")
        payload = {
            "redirect_uri": redirect_uri,
            "grant_type": "authorization_code",
            "code": code,
        }

        client_id = client.http.client_id
        client_secret = client.http.client_secret

        headers = {
            "Authorization": f"Basic {b64encode(':'.join((client_id, client_secret)).encode()).decode()}",
            "Content-Type": "application/x-www-form-urlencoded",
        }

        raw = await client.http.request(route, headers=headers, params=payload)
        token = raw["access_token"]
        refresh_token = raw["refresh_token"]

        return cls.from_token(client, token, refresh_token)

app = FastAPI()
CLI_ID = 'id'
CLI_SEC = 'secret'
SPOTIFY_CLIENT = spotify.Client(CLI_ID, CLI_SEC)
REDIRECT_URI: str = 'http://localhost:5000/spotify/callback'
OAUTH2_SCOPES: Tuple[str] = ('user-top-read',)
OAUTH2: spotify.OAuth2 = spotify.OAuth2(SPOTIFY_CLIENT.id, REDIRECT_URI, scopes=OAUTH2_SCOPES)
SPOTIFY_USERS: Dict[str, spotify.User] = {}

@app.get('/spotify/callback')
async def spotify_callback(code : str):
    key = ''.join(random.choice(string.ascii_uppercase) for _ in range(16))
    SPOTIFY_USERS[key] = await from_code(
        spotify.User,
        SPOTIFY_CLIENT,
        code,
        redirect_uri=REDIRECT_URI,
    )
    response = RedirectResponse(url="/")
    response.set_cookie(key="spotify_user_id", value=key)
    return response

@app.get("/")
async def index(spotify_user_id : Optional[str] = Cookie(None)):
    print('spotify_user_id:', spotify_user_id)
    if spotify_user_id is None:
        return RedirectResponse(url=OAUTH2.url)
    return "Success!"
mental32 commented 3 years ago

Thanks for PR'ing this!

Everything passed when I ran pytest

iirc I was really lazy with the tests so I wouldn't rely on pytest much (my testing strategy was to just write code that used whatever functionality I was testing and if the output looked alright then great)

Below is an example implementation in FastAPI:

Awesome! I'd actually encourage you to submit another PR (or you can include it with this one) that adds this to the /examples subdirectory since it may be useful for others :)