Usage in an asynchronous environment

bloonspy is a synchronous library, but it can be used asynchronously thanks to asyncio.to_thread.

from bloonspy import Client
import asyncio


async def get_race_summary():
    """Print general information about the latest race events."""
    races = await asyncio.to_thread(Client.races)

    # Load all Race objects concurrently
    futures = []
    for race in races:
        futures.append(asyncio.to_thread(race.load_resource))
    await asyncio.gather(*futures)

    for race in races:
        print(f"Race {race.name} went from {race.start} to {race.end}. "
              f"It was on {race.gamemode.difficulty.value} difficulty and "
              f"ended at round {race.end_round}.")


async def other_task_not_getting_blocked():
    """Some other task doing whatever."""
    for _ in range(10):
        print("Task not getting blocked reporting for duty.")
        await asyncio.sleep(1)


async def main():
    """Launches the two tasks above concurrently."""
    await asyncio.gather(
        get_race_summary(),
        other_task_not_getting_blocked()
    )


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

Since all objects are lazy loaded by default, it’s important to call the bloonspy.model.Loadable.load_resource() or bloonspy.model.Event.load_event() explicitely at times, or set the eager parameter to True on methods that support it, and then turn those functions into an asyncio thread. If you don’t, and call a property that’s lazily loaded, the module will make an API call to fetch it, which will result in a blocking function.

Note that not all properties are lazily loaded. Some can be loaded right away, in the example below, Race.name is already loaded when a result of bloonspy.Client.races(), so no need to load the whole resource. This is lined out in the documentation for the function.

async def print_race_difficulty_bad():
    race = await asyncio.to_thread(Client.races)[0]

    # Bad! Since Race.gamemode is not currently loaded,
    # a blocking API call will be made to fetch it.
    print(race.gamemode.difficulty.value)


async def print_race_difficulty_good():
    race = await asyncio.to_thread(Client.races)[0]

    # Good! Load the resource and await it before accessing lazy loaded properties.
    await asyncio.to_thread(race.load_resource)
    print(race.gamemode.difficulty.value)

    race = await asyncio.to_thread(Client.races, eager=True)[1]

    # Also good! The races were loaded thanks to eager=True.
    print(race.gamemode.difficulty.value)


async def print_race_name():
    race = await asyncio.to_thread(Client.races)[0]

    # As pointed out in the API reference page, Race.name already gets
    # loaded when calling Client.races, so there's no need to load the whole
    # resource if not necessary.
    print(race.name)

For more info on lazy and eager loading, see the section below.

Lazy and Eager loading

By default, whenever you request a resource, it will be lazily loaded, meaning not all properties will be available straight away. They will be loaded when you need them instead; this is to limit unnecessary API calls that could make you hit the rate limit. For example:

>>> # The race event is fetched, but only has a few properties available.
>>> # Read the documentation on the Client.get_race function to see which ones.
>>> race = Client.get_race("The_Abyssal_Plane_lfbrs5fb")
>>> # All methods are still available and don't take extra API calls
>>> # to function, unless explicitely specified in the docs.
>>> players = race.leaderboard()
>>> # The gamemode property is not loaded, so Race.load_resource is called to load it.
>>> print(race.gamemode)

In this example, when we lazy load race, only the properties name, start, end, and total_scores will be loaded right away. If we want one that isn’t loaded (in this example, gamemode), it will be fetched automatically via an API call.

If you want race to be fully loaded right away, you can always set the eager parameter to True.

>>> # This will load the race and automatically call Race.load_resource and return a fully loaded object.
>>> race = Client.get_race("The_Abyssal_Plane_lfbrs5fb", eager=True)
>>> # No extra API calls are made since the property is already loaded.
>>> print(race.gamemode)