CSCI 338: Fall 2023

Software Engineering

CSCI 338: Fall 2023

Assignments > Lab 5: AsyncIO, FastAPI, and Python

Due on Thu, 10/12 @ 11:59PM. 6 Points.

Lab 5 Hints

Resources from Tuesday’s class (10/03/2023) to help you get started on Lab 5:

1. Reading & Reference Materials

2. Setup

1. Get the latest files & make a new branch

From within your class-exercises-fall2023 repo on your local computer:

1
2
3
4
5
6
7
8
9
10
class-exercises-fall2023
...
├── lab04           # original copy
├── lab05           # original copy
...
└── your-username
    ├── README.md
    ├── getting-started-app
    ├── lab04       # your copy -- you may not have this one yet if your changes have not yet been reviewed
    └── lab05       # your copy -- you will edit the files in this folder

2. Create the Docker image and container

Credit: Taken from Hayden’s Docker file README.md

Make sure docker is installed and the daemon is running.

1
docker --version

Build the container image. You only need to do this once:

1
docker build -t csci338-lab05:latest .

Run the FastAPI app (Lab 5) using Docker

Run the container using docker run followed by any options and the image name.

The -d option runs the container in “Detached” mode, which frees up your terminal.

The -p 8000:8000 option maps port 8000 on your host machine to the container’s port 8000. Without this option you will not be able to see the app from the browser on your host machine.

The -v ./src:/app option tells docker to map the ./src directory on your host machine to the /app directory inside the container. This allows you to edit the code on your machine and see the updates in the container without restarting the container.

1
docker run -d -p 8000:8000 -v ./src:/app csci338-lab05

Visit http://localhost:8000 and behold! The app running in a container!

3. Activate the Docker Shell

To run and test individual python files using your Docker container, you need to activate the docker shell, and then run your python files from within the Docker environment. To do this:

List the containers:

1
docker container ls -a     

Open the Docker shell for your container of interest:

1
docker exec -it <container-id> bash

Activate the poetry shell (virtual environment) and run the tests:

1
2
poetry shell # activates the python virtual environment
pwd # check that you're in the app directory

Other Useful Docker Commands (just FYI)

Stopping and starting the container

You do not need to run a new container each time you want to work on the code. Instead, you can restart a stopped container using docker start followed by the container name or container id. Some helpful commands:

List all of the containers and their ids:

1
docker container ls -a

Stop the container using docker stop followed by the container name or id.

1
docker stop <container-id>

Start the container using docker start followed by the container name or id.

1
docker start <container-id>
Remove old images

When a new version of the Dockerfile is available you will need to build the image again using docker build and run a new container using docker run.

After a while you may end up with old versions of the image on your system. These can take up quite a bit of disk space, so it is good to check every so often and remove old images from your system.

To show the images on your local machine use docker image ls.

1
docker image ls

Remove any unneeded images using docker image rm followed by a list of one or more image ids or image names.

1
docker image rm <image-id>
Remove old containers

You may want to remove old containers after building and running a new image. List the containers on your system using docker container ls. This will only show running containers, add the `-a’ flag to see stopped containers as well.

1
docker container ls -a

You can remove any containers you need by using docker container rm followed by container name or id.

1
docker container rm <container-id>

3. AsyncIO Walkthrough

Note

This tutorial was generated with the help of ChatGPT!

The asyncio module in Python is a powerful library that provides a framework for writing asynchronous, concurrent code using the async and await syntax. Asynchronous programming allows you to write programs that can perform multiple tasks concurrently without blocking the main execution thread, making it particularly useful for I/O-bound and network-bound operations.

In this tutorial, we will cover the following topics:

  1. What is asyncio?
  2. Basic Concepts
  3. Getting Started with asyncio
  4. Defining a coroutine with async
  5. Running multiple coroutines at once
  6. Working with asyncio Event Loops
  7. Using asyncio with I/O Operations
  8. Error Handling
  9. Timeouts and Cancellation
  10. Real-World Example: Fetching Multiple URLs
  11. Conclusion and Further Resources

1. What is asyncio?

asyncio is a Python library that provides a framework for asynchronous programming. It is designed to handle concurrent I/O operations efficiently and is particularly useful for network programming, web scraping, and other tasks where you might otherwise be waiting for data to arrive.

2. Basic Concepts

Before diving into asyncio, it’s essential to understand some basic concepts:

3. Getting Started with asyncio

To use asyncio, you need Python 3.5 or later. Most modern Python installations come with asyncio built-in, so you don’t need to install it separately.

4. Defining a coroutine with async

To create a coroutine, use the async keyword:

1
2
3
4
5
6
7
8
9
import asyncio

async def my_coroutine():
    print("Hello, asyncio!")

# To run a coroutine, you need an event loop.
loop = asyncio.get_event_loop()
loop.run_until_complete(my_coroutine())
loop.close()

Your Turn

See demo file: lab05/asyncio-exercises/demo1_coroutine.py

To test your python files from Docker, you need to activate the docker shell. To do this:

List the containers:

1
docker container ls -a     

Open the Docker shell:

1
docker exec -it <container-id> /bin/bash

Activate the poetry shell (virtual environment) and run the tests:

1
2
3
4
poetry shell # activates virtual environment
pwd # check that you're in the app directory
cd asyncio-exercises
python demo1_coroutine.py

5. Running multiple asynchronous coroutines at once

You can create and run tasks concurrently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import asyncio


async def coroutine1():
    await asyncio.sleep(3)
    print("Coroutine 1 completed")
    return 6  # fake data to be returned


async def coroutine2():
    await asyncio.sleep(1.5)
    print("Coroutine 2 completed")
    return 9  # fake data to be returned


async def main():
    result1, result2 = await asyncio.gather(
        coroutine1(), coroutine2()
    )

    # Note that the statements below only execute once both
    # coroutines have executed. This is because of the "await"
    # keyword.
    print(result1)
    print(result2)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

Your Turn

See demo file: lab05/asyncio-exercises/demo2_two_coroutines.py

6. Working with asyncio Event Loops

The event loop serves as the core of asyncio, managing the execution of asynchronous tasks (also known as coroutines) and coordinating their execution without blocking the main program’s execution.

In the example below, the sleep statement doesn’t block other coroutines from completing.

1
2
3
4
5
6
7
8
9
import asyncio

async def my_coroutine():
    await asyncio.sleep(1)
    print("Coroutine executed")

loop = asyncio.get_event_loop()
loop.run_until_complete(my_coroutine())
loop.close()

asyncio encourages an event-driven programming style where you define event handlers (coroutines) that react to specific events, such as incoming network data or user input – which we will see in more detail when we work with FastAPI.

7. Using asyncio with I/O Operations

asyncio excels at I/O-bound operations. Here’s an example of reading files asynchronously:

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio
import aiofiles

async def read_file(filename):
    async with aiofiles.open(filename, 'r') as file:
        content = await file.read()
        print(f"Read {len(content)} characters from {filename}")
        print(content)

loop = asyncio.get_event_loop()
loop.run_until_complete(read_file('example.txt'))
loop.close()

Certain kinds of tasks – like File I/O, database calls, and queries over the network – are slow. Given this, if you execute these kinds of commands serially, where they block other processes, this can be inefficient. Hence, running I/O processes as coroutines conserves resources.

Sidebar:

Your Turn

See demo file: lab05/asyncio-exercises/demo3_file_io.py

8. Error Handling

Use try and except to handle errors within coroutines:

1
2
3
4
5
6
7
8
9
10
11
import asyncio

async def my_coroutine():
    try:
        # Your code here
    except Exception as e:
        print(f"An error occurred: {e}")

loop = asyncio.get_event_loop()
loop.run_until_complete(my_coroutine())
loop.close()

9. Timeouts and Cancellation

asyncio allows you to set timeouts and cancel tasks:

1
2
3
4
5
6
7
8
9
10
11
import asyncio

async def timeout_task():
    try:
        await asyncio.wait_for(long_running_task(), timeout=1)
    except asyncio.TimeoutError:
        print("Task timed out")

loop = asyncio.get_event_loop()
loop.run_until_complete(timeout_task())
loop.close()

Sometimes, services are just down (not running, network glitches, etc.). In this case, think about how you would handle errors like this gracefully using timeouts (so as not to use up resources needlessly).

10. Real-World Example: Fetching Multiple URLs

Here’s a simple example of fetching multiple URLs concurrently using aiohttp. This is a good use of asyncio because retrieving resources over the network can be slow and unpredictable, making blocking inefficient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import asyncio
import aiohttp

async def fetch_url(url, message):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            data = await response.text()
            print("Message:", message)
            return data

async def main():
    urls = ["https://openai.com", "https://example.com"]
    tasks = [fetch_url(url, url) for url in urls]

    # delegate execution management to the event loop via the gather method:
    results = await asyncio.gather(*tasks)

    # once all of the coroutines have completed, output the results
    for url, result in zip(urls, results):
        print(f"Fetched {len(result)} bytes from {url}")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

Your Turn

See demo files:

  • lab05/asyncio-exercises/demo4_remote_data.py
  • lab05/asyncio-exercises/demo5_remote_data_timed.py (times the sequential v. parallel to see which one is faster)

For those of you who are new to Python:

1
2
3
4
5
6
7
# this...
tasks = [fetch_url(url, url) for url in urls]

# is a shortcut for this...
tasks = []
for url in urls:
    tasks.append(fetch_url(url, url))

11. Conclusion and Further Resources

This tutorial provides a basic introduction to the asyncio module in Python. Asynchronous programming can significantly improve the performance of I/O-bound tasks and concurrent operations. To dive deeper into asyncio, explore the official documentation and try building more complex applications.

Official asyncio documentation:

Remember that asynchronous programming can be complex, so practice and experimentation are essential for mastering it.

4. Fast API Walkthrough

This Fast API walkthrough will use the same poetry environment as the app application (HW1), but we’ve downgraded the python dependency to >= 3.8 to better accommodate WSL users.

To run the Fast API server, make sure that you are in your version of the lab05/src folder on the CLI. Then run your Docker container using the commands described above in the Docker section.

This should start your webserver on this address: http://127.0.0.1:8000. Go check it out.

Walkthrough of Existing Endpoints

Please open server.py in the src directory and take a look at it. In this file, there are 6 “endpoints” defined that can be accessed through http://127.0.0.1:8000

Route Address
/ http://127.0.0.1:8000
/items/{item_id} http://127.0.0.1:8000/items/123?search_term=chocolate
/data/yelp http://127.0.0.1:8000/data/yelp
/data/spotify http://127.0.0.1:8000/data/spotify
/ui/yelp http://127.0.0.1:8000/ui/yelp
/ui/spotify http://127.0.0.1:8000/ui/spotify

Note that the last 4 endpoints fetch data over the network (remote calls), which present some unique challenges for testing – something we will discuss in more detail when we get to integration testing.

Run the starter tests

To run the tests, you will need to navigate to the Docker shell and run pytest (our Python testing framework). To to this:

Find your container id:

1
docker container ls -a     

Open the Docker shell:

1
docker exec -it <container-id> /bin/bash

Activate the poetry shell (virtual environment) and run the tests:

1
2
3
poetry shell # activates virtual environment
pwd # check that you're in the app directory
pytest --disable-warnings

Your Task

For your Lab 5 submission, you will be creating a few additional endpoints for managing a list of tasks. Begin by doing the following:

  1. rename the current server.py file to server_old.py
  2. rename the current test_server.py file to test_server_old.py
  3. create a brand new server.py file
  4. create a brand new test_server.py file

The requirements are as follows:

1. Implement Endpoints

Start by adding the relevant code and import statements into your new server.py file:

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

import aiohttp
import json

app = FastAPI()


@app.get("/", response_class=HTMLResponse)
async def root():
    return 'Hello world!'

Then, create a FastAPI application with the following endpoints:

2. Data Storage

Implement data storage using an in-memory data structure (e.g., a Python list or dictionary) to store tasks. You don’t need to use a database for this assignment.

3. Validation

Implement validation for request payloads:

4. Testing

5. Bonus (Optional)

5. What to Turn In

Please create a pull request with all of your lab05 files.