Photo by Timothy Dykes on Unsplash
I started at IndigoAg a couple of weeks ago and was thrown into a codebase that leveraged Flask and Restx. Of these are two libraries I have not touched Flask in a long time, and never heard of Restx. Restx has a great tutorial in their documentation, but I was hoping for a more full-featured example to work on. So I created my own and am writing about my thoughts about the process here. If you want to just see the code, it is on Github. Below you will read how I go about creating a new Flask project from scratch using Restx and Docker.
Getting Started
The first thing I did was create a Dockerfile
that would use Python 3.10 and install Poetry for me.
FROM python:3.10-slim
WORKDIR /usr/src/app
RUN pip install poetry
Next, using that Dockerfile
let us add flask and restx.
NOTE: We will want to attach a volume so that the pyproject.toml
and poetry.lock
will be saved locally.
We can run the following command to set up our application:
$ docker build -t todo .
$ docker run -v `PWD`:/usr/src/app todo poetry init
$ docker run -v `PWD`:/usr/src/app todo poetry add flask restx
This will initialize a poetry project.
It will then install the packages and create a poetry.lock
file.
All of this is required to have a repeatable build.
If you are not familiar with Poetry, these are almost equivalent to the setup.py
and requirements.txt
files that are more common.
Once the project is initialized and the lockfile is created, we’ll need to update the Dockerfile
to include them:
FROM python:3.10-slim
WORKDIR /usr/src/app
RUN pip install poetry
COPY pyproject.toml poetry.lock /usr/src/app/
RUN poetry update && poetry install
From now on running docker build -t todo .
will also install Flask and Restx.
Excellent!
The next step will be adding our Flask application to the Docker container.
Let’s first create a folder that will include our application code.
Run mkdir -p todo
to create this folder.
And run touch todo/app.py todo/__init__.py
to create the files we need.
Next add the below code to app.py
:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Back to our Dockerfile
, let’s make sure our code is added to the container and all the environment variable are set up correctly.
FROM python:3.10-slim
WORKDIR /usr/src/app
ENV FLASK_APP=todo/app
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_ENV=development
RUN pip install poetry
COPY pyproject.toml poetry.lock /usr/src/app/
RUN poetry update && poetry install
ADD todo/ /usr/src/app/todo
CMD ["poetry", "run", "flask", "run"]
This is our complete Dockerfile
.
Building the container will give you a full “hello world” example using Flask!
$ docker build -t todo .
$ docker run todo
At this point we have a great working Flask application running within Docker. If you have no interest in learning about Restx, you can stop here. The next section we will build on this example and add rest endpoints for a Todo application.
Flask and Restx
It is great to see how we can get a Flask “Hello World” application up and running in Docker, but I also wanted to explore Restx. To do this, let’s make a few modifications. First, let’s create a folder to hold our API code and the files we’ll need:
$ mkdir -p todo/api
$ touch todo/api/__init__.py todo/api/namespace.py
$ mkdir -p todo/api/resources
$ touch todo/api/resources/__init__.py todo/api/resources/todos.py
$ mkdir -p todo/api/models
$ touch todo/api/models/__init__.py todo/api/models/todos.py
Then we will update our todo/app.py
to leverage our new structure getting ready for the Restx code:
import os
from flask import Flask
from todo.api import register_api
DEBUG = os.getenv("DEBUG", "").lower() == "true"
HOST = os.getenv("HOST", "0.0.0.0")
PORT = os.getenv("PORT", 5000)
app = Flask(__name__)
register_api(app)
if __name__ == "__main__":
app.run(debug=DEBUG, host=HOST, port=PORT)
The new todo/app.py
starts separating some concerns for us.
We move the api code into its own folder, and just reach out to a function register_api()
to connect everything.
In todo/api/__init__.py
we will put the Flask Blueprint for our API.
This code looks like:
from flask import Blueprint
from flask_restx import Api
from todo.api.resources import todos
api_blueprint = Blueprint("api", __name__)
API_VERSION = "v1"
def register_api(app):
app.config["RESTX_ERROR_404_HELP"] = False
app.register_blueprint(api_blueprint, url_prefix=f"/api/{API_VERSION}")
api = Api(
api_blueprint,
version=API_VERSION,
title="Todo API",
description="A simple todo API.",
)
api.add_namespace(todos.ns)
In this file, we add a Flask Blueprint called “api”.
We then create our Restx API object.
Then we attach the todo Restx namespace through the add_namespace
that’s exposed on the Restx API object.
The code above is extendable while allowing us the maximum separation of concerns.
We will have a special namespace for each type of endpoint we want to expose in our API.
It is a eloquent solution.
But what does this code look like for the todo endpoints?
We first create the todo Restx namespace.
We will add it to todo/api/namespaces.py
like this:
from flask_restx import Namespace
todo_ns = Namespace("todos", description="Todos")
This will tell our application that we are expecting an API endpoint for todos.
After creating the todo Restx namespace, we can implement the model representation in todo/api/models/todo.py
.
We will define the todo model and create a Data Access Object (DAO) to access it.
DAOs are nice because even though this example saves “todos” to memory, we can change very little code to use a database to persist the data.
from flask_restx import fields
from todo.api.namespaces import todo_ns as ns
todo = ns.model(
"Todo",
{
"id": fields.Integer(readonly=True, description="The task unique identifier"),
"task": fields.String(required=True, description="The task details"),
},
)
class TodoDAO:
def __init__(self):
self.counter = 0
self.todos = []
def get(self, id):
for todo in self.todos:
if todo["id"] == id:
return todo
ns.abort(404, "Todo {} doesn't exist".format(id))
def create(self, data):
todo = data
todo["id"] = self.counter = self.counter + 1
self.todos.append(todo)
return todo
def update(self, id, data):
todo = self.get(id)
todo.update(data)
return todo
def delete(self, id):
todo = self.get(id)
self.todos.remove(todo)
DAO = TodoDAO()
DAO.create({"task": "Build an API."})
DAO.create({"task": "?????"})
DAO.create({"task": "profit!"})
Finally we will need to create the CRUD routes for todos.
We will add the following to the todo/api/resources/todo.py
file:
from flask_restx import Resource
from todo.api.namespaces import todo_ns as ns
from todo.api.models.todos import todo, DAO
@ns.route("/")
class TodoList(Resource):
@ns.doc("list_todos")
@ns.marshal_list_with(todo)
def get(self):
return DAO.todos
@ns.doc("create_todo")
@ns.expect(todo)
@ns.marshal_with(todo, code=201)
def post(self):
return DAO.create(ns.payload), 201
@ns.route("/<int:id>")
@ns.response(404, "Todo not found")
@ns.param("id", "The task identifier")
class Todo(Resource):
@ns.doc("get_todo")
@ns.marshal_with(todo)
def get(self, id):
return DAO.get(id)
@ns.doc("delete_todo")
@ns.response(204, "Todo deleted")
def delete(self, id):
DAO.delete(id)
return "", 204
@ns.expect(todo)
@ns.marshal_with(todo)
def put(self, id):
return DAO.update(id, ns.payload)
Above is the Restx Resource code that covers CRUD for our todos. There is the ability to create a todo, read all todos, update an individual todo, and delete an individual todo. With the DAO we created before, these endpoints look pretty trivial. But, remember this is a simplistic example without authentication or anything. That will add more complexity, but should not be too difficult to add in the above file.
Conclusion
With these files, we have a full-featured rest application for our todos that runs in docker. I hope you were able to follow along and learn something. If you have any input, comments, or questions feel free to find me on Twitter. We can discuss! Also, remember, the entire code example can be found on Github.