Skip to content

Basic Quart app

To begin it makes sense to setup a basic API that responds to pings (requests) with a pong. This firstly requires Quart as a (full) dependency,

Run this command in backend/

poetry add quart

which installed Quart 0.14.1. For the basic setup we can add a simple ping route, which simply responses with pong when requested. I like to group routes like this into a control blueprint, by adding the following to backed/src/backend/blueprints/control.py,

from quart import Blueprint, ResponseReturnValue

blueprint = Blueprint("control", __name__)

@blueprint.route("/control/ping/")
async def ping() -> ResponseReturnValue:
    return {"ping": "pong"}

and then the following to backed/src/backend/run.py to create the app using the factory pattern,

from quart import Quart

from backend.blueprints.control import blueprint as control_blueprint

def create_app() -> Quart:
    app = Quart(__name__)
    app.register_blueprint(control_blueprint)
    return app

if __name__ == "__main__":
    app = create_app()
    app.run()

Note

Using a Blueprint and the factory pattern to create the app will seem overly complex at this stage (it is 😄). As we are about to add a few blueprints and further testing though, this is worthwhile doing from the outset. For quick prototyping though I'd recommend the Quart quickstart (all in one file) setup.

Now there is code to create the app with a ping route we should setup the tooling such that the server starts locally and serves requets. As with the backend tooling we need to add to backend/pyproject.toml,

[tool.poetry.scripts]
start = "scripts:start"
...

and to backend/scripts.py

def start() -> None:
    _check_call_quiet(["python", "src/backend/run.py"])

which allows,

Run this command in backend/

poetry run start

to start our application. You can test it works manually using the curl tool,

Run this command in any location

curl localhost:5000/control/ping/

or visiting localhost:5000/control/ping/ in your browser.

Testing

Lets test this route, first by setting up a test app fixture via this code in backend/tests/conftest.py,

from typing import AsyncGenerator

import pytest
from quart import Quart

from backend.run import create_app

@pytest.fixture(name="app", scope="function")
async def _app() -> AsyncGenerator[Quart, None]:
    app = create_app()
    async with app.test_app():
        yield app

which allows any test to accept an app argument set to the value yielded by this fixture. This allows a test in backend/tests/blueprints/test_control.py,

import pytest
from quart import Quart

@pytest.mark.asyncio
async def test_control(app: Quart) -> None:
    test_client = app.test_client()
    response = await test_client.get("/control/ping/")
    assert (await response.get_json())["ping"] == "pong"

Note

It is quite common to mistakenly write,

assert await response.get_json()["ping"] == "pong"

which will fail with a coroutine cannot be indexed error as the coroutine returned by response.get_json() must be awaited before it is indexed.

Full structure

If you've setup the local backend tooling, and the basic API setup described here you should have this folder structure,

tozo
└── backend
    ├── poetry.lock
    ├── pyproject.toml
    ├── scripts.py
    ├── setup.cfg
    ├── src
    │   └── backend
    │       ├── blueprints
    │       │   ├── __init__.py
    │       │   └── control.py
    │       ├── __init__.py
    │       └── run.py
    └── tests
        ├── blueprints
        │   ├── __init__.py
        │   └── test_control.py
        ├── __init__.py
        └── conftest.py

Note

The __init__.py files are empty, but exist to make the directories importable as python modules.

Note

The testing code is located in a folder structure that shadows the src structure as this helps locate the tests i.e. the tests for src/backend/blueprints/control.py are in tests/blueprints/test_control.py.