Help:Toolforge/My first Python ASGI tool
Prior to Build Service, it was not possible to deploy Python ASGI applications to Toolforge due to the Webservice framework applying the uWSGI application server onto all Python web services. As Build Service lets the user specify the web server they want to use in their application, it is now possible to deploy ASGI applications in addition to WSGI.
This stub webservice is designed to get a sample Python ASGI application deployed onto Toolforge using the new Build Service, as quickly as possible. The application is written using the FastAPI framework.
The guide will teach you how to:
- Create a new tool
- Build and deploy a Python 3 ASGI webservice using Toolforge Build Service.
Getting started
Prerequisites
Skills
- Basic knowledge of Python
- Basic knowledge of SSH
- Basic knowledge of the Unix command line
- Basic knowledge of Git
Accounts
Step-by-step guide
Step 1: Create a new tool account
- Follow the Toolforge quickstart guide to create a Toolforge tool and SSH into Toolforge.
- For the examples in this tutorial,
sample-python-buildpack-app
is used to indicate places where your unique tool name is used in another command.
- For the examples in this tutorial,
- Make sure to create a git repository for the tool, you can get one like this:
- Log into the toolforge admin page
- Select your tool
- On the left side panel, under
Git repositories
clickcreate repository
- Copy the url in the
Clone
section- There's a private url, that we will use to clone it locally, starting with "git":
git@gitlab.wikimedia.org:toolforge-repos/sample-python-buildpack-asgi.git
- And a public one, that we will use to build the app in toolforge, starting with "https":
https://gitlab.wikimedia.org/toolforge-repos/sample-python-buildpack-asgi.git
- There's a private url, that we will use to clone it locally, starting with "git":
Step 2: Create a basic FastAPI ASGI application
The application we are going to build is a simple random quote generator that exposes an API. The quotes are fetched from an open-source third-party API which does not require authentication.
- What is FastAPI?
FastAPI is a modern, high-performance web framework for building APIs with Python. Its key features include automatic data validation, serialization, and documentation using OpenAPI. It has asynchronous support which makes it suitable for high-concurrency applications such as ML-applications and data processing systems.
Let's get started creating the web application.
1. Clone your tool git repository
You will have to clone the tool repository to be able to add code to it, on your local computer (with git installed) you can run:
laptop:~$ git clone git@gitlab.wikimedia.org:toolforge-repos/sample-python-buildpack-asgi.git
laptop:~$ cd sample-python-buildpack-asgi
This will clone our project from GitLab. For now, the folder is empty but it contains an already initialized git repository. We are ready to start.
2. Install the application dependencies
It is good practice to create a virtual environment for your Python project. There are several ways of doing so; use your preferred one. For an in-depth guide, see Python/Development Tools.
laptop:~sample-python-buildpack-asgi$ pip install fastapi httpx 'uvicorn[standard]' gunicorn
Later, we will export our dependencies to a file called requirements.txt
. This file is required for the Build Service to install our project's dependencies.
Optionally, if we want to specify a Python version, we can create a file called runtime.txt
. If we don't, our project will install and use the latest Python version available to the software that builds our application image under the hood, currently 3.12.1. Here is how we could specify a different version:
laptop:~sample-python-buildpack-asgi$ echo "python-3.10.2" > runtime.txt
3. Create a basic ASGI application
# -*- coding: utf-8 -*-
#
# This file is part of the Toolforge Python ASGI tutorial
#
# Copyright (C) 2023 Slavina Stefanova and contributors
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/")
async def hello():
return {"Hello": "World"}
Code on Toolforge must always be licensed under an Open Source Initiative (OSI) approved license. See the Right to fork policy for more information on this Toolforge policy.
We are now ready to run the application locally:
laptop:~sample-python-buildpack-asgi$ uvicorn app:app --reload
You should now be able to navigate to http://127.0.0.1:8000 and see the hello world JSON. Nice! Let's add the code to fetch a random quote, and create an API endpoint for it.
# -*- coding: utf-8 -*-
#
# This file is part of the Toolforge Python ASGI tutorial
#
# Copyright (C) 2023 Slavina Stefanova and contributors
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
async def fetch_random_quote():
'''Fetches a random quote from the Quotable API, an open-source API (https://github.com/lukePeavey/quotable).
'''
async with httpx.AsyncClient() as client:
response = await client.get('https://api.quotable.io/random')
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail="Unable to fetch quote")
@app.get("/")
async def hello():
return {"Hello": "World"}
@app.get("/quote")
async def read_random_quote():
quote_data = await fetch_random_quote()
return quote_data
We now have a GET API endpoint called /quote
. One of the really nice features of FastAPI is that it comes with built-in OpenAPI documentation. Try it out by navigating to http://127.0.0.1:8000/docs.
Step 3: Prepare the application for deployment
1. Create a requirements.txt
file
laptop:~sample-python-buildpack-asgi$ pip freeze > requirements.txt
Every time you add new dependencies to your project, this needs to be run anew.
NOTE: There's currently no other way to specify your dependencies, you can find some workaround in task T353762
2. Create the Procfile
The Procfile is based on heroku's procfile, though we don't support all it's features. For now, we only use the web
entry point to get the command your server will be started with:
laptop:~sample-python-buildpack-asgi$ cat > Procfile << EOF
web: gunicorn app:app -k uvicorn.workers.UvicornWorker --workers=4 --timeout 60 --bind 0.0.0.0
EOF
You can try this command locally. Note that it will default to using port 8000. If this port is already is use by another application, you can specify a different port for Gunicorn by changing the --bind
argument, such as --bind 0.0.0.0:8080
to use port 8080 instead.
The --timeout
argument isn't strictly necessary, but it can help when deploying more resource-intensive applications to Toolforge.
- Understanding Uvicorn and Gunicorn in ASGI Applications
Uvicorn: Uvicorn is an ASGI server implementation, using uvloop and httptools. Primarily, it serves as an ASGI server for running asynchronous Python web applications built with frameworks like FastAPI or Starlette. It's known for its speed and efficiency in handling asynchronous operations.
Gunicorn with Uvicorn Workers: Gunicorn, traditionally a WSGI server for synchronous Python web applications, can be extended to support asynchronous applications by using Uvicorn workers. When used with Uvicorn workers (-k uvicorn.workers.UvicornWorker
), Gunicorn manages multiple Uvicorn worker processes. This setup allows your FastAPI application to handle a higher load than what a single Uvicorn worker could manage on its own, providing a balance between Uvicorn's high performance in asynchronous tasks and Gunicorn's robust process management.
How They Work Together: In this configuration, Gunicorn acts as the main process manager, handling things like worker process scaling and network traffic distribution. Each worker process runs an instance of the Uvicorn server. This combination is ideal for production deployments of FastAPI applications, combining the concurrency benefits of Uvicorn and the process management strengths of Gunicorn.
3. Commit your changes and push
laptop:~sample-python-buildpack-asgi$ git add .
laptop:~sample-python-buildpack-asgi$ git commit -m "First commit"
laptop:~sample-python-buildpack-asgi$ git push origin main
EOF
- Build the image
Now we have to ssh to login.toolforge.org
and start the build for the image:
laptop:~sample-python-buildpack-aasi$ ssh login.toolforge.org # or the equivalent with PuTTY
user@tools-sgebastion-10$ become sample-python-buildpack-asgi
tools.sample-python-buildpack-asgi@tools-sgebastion-10$ toolforge build start https://gitlab.wikimedia.org/toolforge-repos/sample-python-buildpack-asgi.git
- Wait for the build to finish
You can check the status of the build like this:
tools.sample-python-buildpack-app@tools-sgebastion-10:~$ toolforge build show
You have to wait for the status to be ok(Succeeded)
.
- Start the webservice
tools.sample-python-buildpack-asgi@tools-sgebastion-10$ toolforge webservice buildservice start --mount=none
Starting webservice.
Once the webservice is started, navigate to https://sample-python-buildpack-asgi.toolforge.org
in your web browser, and see a 'Hello World!' message. It might take a few minutes until it is reachable. Again, the API docs will be available at https://sample-python-buildpack-asgi.toolforge.org/docs
Notes
You can see the code used in this example here: https://gitlab.wikimedia.org/toolforge-repos/sample-python-buildpack-asgi
Troubleshooting
See Help:Toolforge/Build_Service#Troubleshooting.
See also
- Git repository 'tool-my-first-flask-oauth-tool' on Phabricator
- My-first-flask-oauth-tool on Toolforge
- Help:Toolforge/My first Buildpack Python tool
- Help:Toolforge/My first Django OAuth tool
- Help:Toolforge/My first NodeJS OAuth tool
Communication and support
Support and administration of the WMCS resources is provided by the Wikimedia Foundation Cloud Services team and Wikimedia movement volunteers. Please reach out with questions and join the conversation:
- Chat in real time in the IRC channel #wikimedia-cloud connect or the bridged Telegram group
- Discuss via email after you have subscribed to the cloud@ mailing list
- Subscribe to the cloud-announce@ mailing list (all messages are also mirrored to the cloud@ list)
- Read the News wiki page
Use a subproject of the #Cloud-Services Phabricator project to track confirmed bug reports and feature requests about the Cloud Services infrastructure itself
Read the Cloud Services Blog (for the broader Wikimedia movement, see the Wikimedia Technical Blog)