As we have already mentioned multiple times in this chapter, asynchronous programming is a great tool for handling I/O bound operations. So, it's time to build something more practical than simple printing of sequences or asynchronous waiting.
For the sake of consistency, we will try to handle the same problem we solved previously with the help of multithreading and multiprocessing. So, we will try to asynchronously fetch some data about current currency exchange rates from an external resource through the network connection. It would be great if we could use the same requests library as in the previous sections. Unfortunately, we can't. Or to be precise, we can't do that effectively.
Unfortunately, requests do not support asynchronous I/O with the async and await keywords. There are some other projects that aim to provide some concurrency to the requests project, but they either rely on Gevent (grequests, refer to https://github.com/kennethreitz/grequests) or thread/process pool execution (requests-futures, refer to https://github.com/ross/requests-futures). Neither of these solve our problem.
Knowing the limitation of the library that was so easy to use in the previous examples, we need to build something that will fill the gap. The foreign exchange rates API is really simple to use, so we just need to use some natively asynchronous HTTP library for that job. The standard library of Python in version 3.7 still lacks any library that would make asynchronous HTTP requests as simple as calling urllib.urlopen(). We definitely don't want to build the whole protocol support from scratch, so we will use a little help from the aiohttp package that's available on PyPI. It's a really promising library that adds both client and server implementations for asynchronous HTTP. Here is a small module built on top of aiohttp that creates a single get_rates() helper function that makes requests to the foreign exchange rates API service:
import aiohttp
async def get_rates(session: aiohttp.ClientSession, base: str):
async with session.get(
f"https://api.exchangeratesapi.io/latest?base={base}"
) as response:
rates = (await response.json())['rates']
rates[base] = 1.
return base, rates
Let's assume that this code is stored in a module named asyncrates that we are going to use later. Now, we are ready to rewrite the example used we discussed multithreading and multiprocessing. Previously, we used to split the whole operation into the following two separate steps:
- Perform all requests to an external service in parallel using the fetch_place() function.
- Display all the results in a loop using the present_result() function.
But because cooperative multitasking is something completely different from using multiple processes or threads, we can slightly modify our approach. Most of the issues raised in the Using one thread per item section are no longer our concern. Coroutines are non-preemptive, so we can easily display results immediately after HTTP responses are awaited. This will simplify our code and make it clearer as follows:
import asyncio
import time
import aiohttp
from asyncrates import get_rates
SYMBOLS = ('USD', 'EUR', 'PLN', 'NOK', 'CZK')
BASES = ('USD', 'EUR', 'PLN', 'NOK', 'CZK')
async def fetch_rates(session, place):
return await get_rates(session, place)
async def present_result(result):
base, rates = (await result)
rates_line = ", ".join(
[f"{rates[symbol]:7.03} {symbol}" for symbol in SYMBOLS]
)
print(f"1 {base} = {rates_line}")
async def main():
async with aiohttp.ClientSession() as session:
await asyncio.wait([
present_result(fetch_rates(session, base))
for base in BASES
])
if __name__ == "__main__":
started = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
elapsed = time.time() - started
print()
print("time elapsed: {:.2f}s".format(elapsed))
That's fairly easy for a simple API. But sometimes, you need a specialized client library that isn't asynchronous and cannot be easily ported. We will cover such a situation in the next section.