Using the API to run ASGI sites on PythonAnywhere (beta)

Disclaimer

Deployment of ASGI-based (and other async) websites on PythonAnywhere is an experimental feature. Some important limitations to know about:

  • There is no support for static file mappings.
  • There is a very limited web UI for creating and managing async websites. Contact support@pythonanywhere.com if you would like us to enable it for your account.
  • We do not guarantee that the command line syntax and the API interface will remain the same.
  • We have not worked out the long-term pricing for ASGI sites, which will probably differ from the way we charge for traditional WSGI ones. We're 99.9% certain that there will be a way to host them in a free plan, though!

If you are brave enough to try it, here is a quick guide how to do it using our API.

Note: if you're just getting started with ASGI-based sites on PythonAnywhere, we recommend that you set them up using our pa command-line tool. This provides the same functionality as the API, but wrapped up so that you can quickly and easily use it from Bash. The API is generally only a better option if you're trying to automate things.

We have a help page on using the pa command-line tool, and we suggest that you go through at least one of the examples there before trying the API.

If you've already done that, then read on!

Prerequisites

API token

First, you will need an API token. This page will show you how to get that.

Now you can use our experimental API to deploy your website. Note down the API token, as you'll be using it in the code samples below.

Creating a simple website to test against

We'll use FastAPI as the example in this page, but the others are similar -- you'd just need to change the command provided when creating the site, just as you would when using the pa command-line tool.

Virtual environment

Firstly, create a virtual environment with requests, fastapi and uvicorn installed.

To create an environment called fast_venv run:

mkvirtualenv fast_venv --python=python3.10

...and then install the requirements:

pip install requests "uvicorn[standard]" fastapi

The code of your website

Create a directory ~/my_fastapi/. In that directory, create a file called main.py, with the following code:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message":"Hello from FastAPI"}

Managing your website via the API

Creating it

Now you can access the API to create your website. Run python in your Bash console, and then use the following code. Don't forget to replace YOUR TOKEN HERE with your actual API token, YOURUSERNAME with your PythonAnywhere username, and to use the correct values for pythonanywhere_host and pythonanywhere_domain based on whether your account is on our US-based system at www.pythonanywhere.com, or our EU-based system at eu.pythonanywhere.com.

from pprint import pprint
from urllib.parse import urljoin

import requests


api_token = "YOUR TOKEN HERE" # Update to match your real API token
headers = {"Authorization": f"Token {api_token}"}

username = "YOURUSERNAME"  # update to match your username!

pythonanywhere_host = "www.pythonanywhere.com"  # or "eu.pythonanywhere.com" if your account is hosted on our EU servers
pythonanywhere_domain = "pythonanywhere.com"  # or "eu.pythonanywhere.com"

# make sure you don't use this domain already!
domain_name = f"{username}.{pythonanywhere_domain}"

api_base = f"https://{pythonanywhere_host}/api/v1/user/{username}/"
command = (
    f"/home/{username}/.virtualenvs/fast_venv/bin/uvicorn "
    "--uds ${DOMAIN_SOCKET} "
    "my_fastapi.main:app "
)

response = requests.post(
    urljoin(api_base, "websites/"),
    headers=headers,
    json={
        "domain_name": domain_name,
        "enabled": True,
        "webapp": {"command": command}
    },
)
pprint(response.json())

If you've used the pa command-line tool previously, you'll probably spot that the domain_name and the command are the same as the parameters that you would use with it. The rest is the boilerplate you need to write to access the API using requests.

If everything was successful, you should see something like:

{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
 'enabled': True,
 'id': 42,
 'user': 'YOURUSERNAME,
 'webapp': {'command': '/home/YOURUSERNAME/.virtualenvs/fast_venv/bin/uvicorn '
                       'my_fastapi.main:app --uds ${DOMAIN_SOCKET}',
            'domains': [{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
                         'enabled': True}],
            'id': 42}}

Now, if you go to the website URL defined in domain_name you should get

{"message":"Hello from FastAPI"}

You have a working FastAPI website hosted on PythonAnywhere, created via our API.

Getting and listing websites

If you do a "get" request to the websites/ API endpoint, you'll get a list of your websites:

# the same setup as above...

endpoint = urljoin(api_base, "websites/")
response = requests.get(endpoint, headers=headers)
pprint(response.json())

...will give you something like this:

[{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
  'enabled': True,
  'id': 42,
  'user': 'YOURUSERNAME,
  'webapp': {'command': '/home/YOURUSERNAME/.virtualenvs/fast_venv/bin/uvicorn '
                        'my_fastapi.main:app --uds ${DOMAIN_SOCKET}',
             'domains': [{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
                          'enabled': True}],
             'id': 42}}]

Likewise, you can get the information about a particular website by adding its domain name, and a trailing slash, to the URL:

# the same setup as above...

endpoint = urljoin(api_base, f"websites/{domain_name}/")
response = requests.get(endpoint, headers=headers)
pprint(response.json())

You'll get something like this:

{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
 'enabled': True,
 'id': 42,
 'user': 'YOURUSERNAME,
 'webapp': {'command': '/home/YOURUSERNAME/.virtualenvs/fast_venv/bin/uvicorn '
                       'my_fastapi.main:app --uds ${DOMAIN_SOCKET}',
            'domains': [{'domain_name': 'YOURUSERNAME.pythonanywhere.com',
                         'enabled': True}],
            'id': 42}}

Using a custom domain for your web app

If you are using a custom domain, there will be an extra field called cname in the output above. This is the CNAME that you can use in your DNS settings for your web app. For more details on setting up DNS for a custom domain, see:

Enabling HTTPS for custom domains

# the same setup as above...

endpoint = urljoin(api_base, f"domains/{domain_name}/ssl/")
response = requests.post(endpoint, headers=headers, json={'cert_type': 'letsencrypt-auto-renew'})
pprint(response.json())

You'll get something like this:

{'status': 'OK'}

to let you know that it has been applied.

Reload

If you have changed the code of your website, you need to reload it using the API to pick up the new code:

# the same setup as above...

endpoint = urljoin(api_base, f"websites/{domain_name}/reload")
response = requests.post(endpoint, headers=headers)
print(response)

Disabling and enabling

If you want to temporarily disable your site without deleting it, or if you've previously disabled it and you wnat to enable it, here's what you do:

# the same setup as above...

endpoint = urljoin(api_base, f"websites/{domain_name}/")
# disable:
response = requests.patch(endpoint, headers=headers, json={"enabled": False})
print(response)
# enable:
response = requests.patch(endpoint, headers=headers, json={"enabled": True})
print(response)

However, enabled is the only property of the website that you can patch -- if you'd like to update the serving command, you'll need to delete the current site, and re-deploy it with a new one.

Delete

To delete your website, use the following code -- mind the / at the end of the endpoint!

# the same setup as above...

response = requests.delete(
    urljoin(api_base, f"websites/{domain_name}/"),
    headers=headers
)
print(response)

You should get a 204 status code on successful delete.