#🔒 Refactoring api calls with asyncio

32 messages · Page 1 of 1 (latest)

thorn grail
#

Hello, earlier today i created this help post: https://discord.com/channels/267624335836053506/1251968531830935582

I have some code that makes anywhere from 2 to 60 ish API calls, and I was told to do those asynchronously. So I refactored it and it works, but I'm still a little confused. My main concern is did I put way too many async/await statements? and secondly, how would I go about calling this from FastAPI?

Here's the updated code:

import requests
import datetime
from dateutil.parser import parse, ParserError
from dateutil.relativedelta import relativedelta  
import asyncio
import aiohttp

def get_dates():
    try:
        dates = requests.get("https://raw.githubusercontent.com/mhollingshead/billboard-hot-100/main/valid_dates.json")
        dates.raise_for_status()
    except requests.exceptions.HTTPError:
        print("An error occured fetching the data, sorry!")
    except Exception as err:
        print("There was an error!\n {err}")
    return dates.json()

def relevant_dates(birthday, dates):
    today = datetime.date.today()
    list_of_relevant_dates = list()
    while birthday < today:
        # the min() function sorts by timedelta between birthday and date. abs() is required because of negative values,
        # we want values closest to 0.
        list_of_relevant_dates.append(min(dates, key=lambda d: abs(birthday-d) if d <= birthday else datetime.timedelta.max))
        birthday = birthday + relativedelta(years=1)
    return list_of_relevant_dates

def get_relevent_charts_tasks(session, list_of_dates):
    URL = "https://raw.githubusercontent.com/mhollingshead/billboard-hot-100/main/date/"
    tasks = []
    for date in list_of_dates:
        tasks.append(session.get("{}{}.json".format(URL, date.isoformat())))
    return tasks

async def relevant_charts(list_of_dates):
    async with aiohttp.ClientSession() as session:
        tasks = get_relevent_charts_tasks(session, list_of_dates)
        responses = await asyncio.gather(*tasks)
    list_of_charts = [ await response.json(content_type=None) for response in responses ]
    return list_of_charts
"""    for date in list_of_dates:
        try:
            chart = requests.get("{}{}.json".format(URL, date.isoformat()))
            list_of_charts.append(chart.json())
        except requests.exceptions.HTTPError:
            pass"""

async def get_songs(chart_list, number_of_songs):
    dict_of_songs = dict()
    list_of_charts = await chart_list
    for chart in list_of_charts:
        songs = dict()
        i = 0
        while i < number_of_songs:
            try:
                songs[i+1] = {"title": chart['data'][i]['song'], "artist": chart['data'][i]['artist']}
                i += 1
            except IndexError:
                break
        #year_and_songs = [chart['date'], songs]
        dict_of_songs[chart['date']] = songs
    return dict_of_songs

async def get_birthday_songs(birthday, number_of_songs):
    birthday = parse(birthday).date()
    number_of_songs = int(number_of_songs)
    list_of_parsed_dates = [ parse(date).date() for date in get_dates() ]
    dict_of_songs = await get_songs(relevant_charts(relevant_dates(birthday, list_of_parsed_dates)), number_of_songs)
    return dict_of_songs

async def main():
    birthday = input("Please enter your birthday (YYYY-MM-DD): ")
    number_of_songs = input("How many songs do you want per year?: ")
    dict_of_songs = await get_birthday_songs(birthday, number_of_songs)
    print(dict_of_songs)

if __name__ == "__main__":
    asyncio.run(main())

Thank you!

peak condorBOT
#

@thorn grail

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

thorn grail
#

^ I feel like I just added async to every function and await to every time i call an async function lol. the later parts of the program aren't really doing anything asynchronously so i don't really understand why this is necessary

frosty dagger
#

looks like there's still a lot of requests stuff left over, even when you have aiohttp. you generally want to just use one or the other, not both

#

having a lot of async/awaits is normal. it may even signify a good thing, that you're using async/await where it really shines

thorn grail
#

@frosty dagger requests is only used once in the first function, ill change that over in a bit

frosty dagger
#

yah, but I/O blocking should not happen in an async event loop

#

requests is blocking IO

thorn grail
#

last portion -> synchronous, using all the data gathered in the middle portion

#

@frosty dagger does the I/O blocking in the initial part with requests really matter, considering it needs to finish first before i do anything else?

#

i think i figured it out, i should probably do asyncio.run() earlier in the logic, right?

finite thistle
#

in general I would recommend using async for everything if you want to use it at all

it can be confusing and hard to swap between 'sync' code and async code, and if you are already using async for parts of it, changing the rest to use async shouldn't be too bad

thorn grail
#

if i do async everything

frosty dagger
#

i believe you may be mistaking how async works

#

wanting the results of an operation doesn't necessitate synchronous operations

#

async simply allows you to perform other tasks while waiting on those results (which take a long time, from a computer's perspective)

thorn grail
#

i think i get it now, this works swimmingly, tysm @frosty dagger

#

adding onto this, the function gets called with fastapi. how would i go about making it so that each API call is an unique instance that runs independently of other requests? so for example:
client 1 makes GET request, client 2 makes GET request after client 1
I want it so that client 2’s request doesn’t have to wait for client 1’s request to be processed

frosty dagger
frosty dagger
thorn grail
frosty dagger
#

unless something was done incorrectly

thorn grail
# frosty dagger unless something was done incorrectly

no, i’m just assuming i had to do something more, i haven’t tested with multiple clients yet. here’s the code for it btw:


@app.get("/playlist")
async def playlist(b, s):
    try:
        playlist_json = await get_birthday_songs(b, s)
    except (TypeError, ParserError):
        raise HTTPException(status_code=404, detail="Unreadable input. Please try again.")
    return playlist_json
frosty dagger
#

that'll be fine

thorn grail
frosty dagger
peak condorBOT
#
Python help channel closed

This help channel has been closed and it's no longer possible to send messages here. If your question wasn't answered, feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.