In database terminology primary key refers to the column in a table that's intended to be the primary way of identifying rows. Each table must have exactly one, and it needs to be unique. This is usually some kind of a unique identifier associated with objects presented by the table, or if such an identifier doesn't exist simply a running ID number (which is incremented automatically).
Flask API Project Layout¶
This page contains additional material that is not strictly needed in your course project, but is useful general knowledge about what API projects generally look like. Exercise 3 will make some references to this material.
While having everything in one file is convenient for very small applications and just generally poking around to see how Flask works, a more serious project needs a more serious layout - srsly. Flask's own tutorial has an adequate project layout example for web applications. They key difference is that we are using ORM in the form of Flask SQLAlchemy, and we are making a RESTful API instead of a web application. This page includes considerations and examples regarding how to lay out your project.
An example project using this layout can be found from here
Layout Structure¶
Your ambitions about the project generally define how elaborate structure you want it to have. A project with a more complex layout spanning multiple folders and files always requires some amount of structural work before you get to write a single view. On the other hand, figuring out how to organize components of your project in advance makes it much easier to expand your scope. At minimum you should have three files: one for your database models, one for your views and one for your tests; and a
static
folder. However this guide takes a bit more ambitious approach, resulting in a layout like this:/your/project/root/ ├── MANIFEST.in ├── README.md ├── setup.py ├── sensorhub │ ├── __init__.py │ ├── api.py │ ├── models.py │ ├── utils.py │ ├── resources/ │ │ ├── __init__.py │ │ ├── deployment.py │ │ ├── location.py │ │ ├── measurement.py │ │ └── sensor.py │ └── static/ │ └── schema/ └── tests ├── api_test.py ├── db_test.py └── utils.py
Doing this results in a project that expands quite nicely in terms of adding new resources - each resource (or rather pair of resources) is placed into its own file. The two files that are most likely to grow out of control are models.py and api_test.py. Test modules are very easy to add when using pytest, so the only real potential future consideration is how to split the models into multiple files.
Using Application Factory¶
Up until now we have created our Flask application simply by putting
app = Flask(__name__)
into our glorious single file application. Now we're going to upgrade this single line into a function that creates our application. The primary reason to do this is to manage different run configurations. Namely, most web applications have a need for three separate configurations:- Development: the application is run on the developer's computer using a development database, maximum debug information and usually with relaxed security measures
- Production: the application is run on a production server with a database containing real data, without debug tools and with all required security measures in place
- Testing: generally same as development, but uses a temporary database for each test case
Another difference is using the special
__init__.py
file (see the layout in the previous section) to contain this function rather than using a named module like app.py that we used previously.import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# Based on http://flask.pocoo.org/docs/1.0/tutorial/factory/#the-application-factory
# Modified to use Flask SQLAlchemy
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY="dev",
SQLALCHEMY_DATABASE_URI="sqlite:///" + os.path.join(app.instance_path, "development.db"),
SQLALCHEMY_TRACK_MODIFICATIONS=False
)
if test_config is None:
app.config.from_pyfile("config.py", silent=True)
else:
app.config.from_mapping(test_config)
try:
os.makedirs(app.instance_path)
except OSError:
pass
db.init_app(app)
return app
Configuration can now be defined in one of three ways:
- provide the configuration as an argument when calling create_app - this is for testing
- have config.py in the application's instance folder- intended for production
- do neither of the above and use the hardcoded configuration in the function - default for development
Flask knows how to call this function. In order to start the server, just export the package name and run Flask in the sensorhub folder:
export FLASK_APP=sensorhub export FLASK_ENV=development flask run
The purpose of using Flask's instance path feature is to separate deployment specific files (e.g. configuration files) from files that are part of the project. As they are not part of the project, there's no need to worry about ignoring them when committing changes to your project with git etc.
Using the Command Line Interface¶
Some management operations in web development are often done from the command line. This can include operations like creating the initial database or
database migration
. Flask utilizes Click to define terminal commands. Starting the application with flask run
is one of these commands, but you can also define your own. This can be done by importing Click and using the click.command
decorator to register a function. For now we're going to do only one command line function, and it is quite simple. The function along with its two required decorators and imports looks like this:import click
from flask.cli import with_appcontext
@click.command("init-db")
@with_appcontext
def init_db_command():
db.create_all()
In our project we are going to define this function in models.py because that's where our
models
are. This ensures that when this function is called, the models have been registered. This means that we also need to import the SQLAlchemy object that was created in __init__.py by adding from sensorhub import db
. Then we also need to actually tell Flask that this command exists. The place this should be done is inside the create_app function. Also, in order to avoid circular import issues, it's better to import models inside the function. This ensures that everything imported from __init__.py is fully loaded before trying to load the models. So we drop these two lines inside the function (between db.init_app and return):from . import models
app.cli.add_command(models.init_db_command)
Now you can type
flask init-db
on the command line to setup your database. This will place the database file inside the app's instance folder
. The location depends on your setup - you can see it by adding print(app.instance_path)
somewhere. When writing this guide the instance folder was placed in the virtual environment's var folder (i.e. /path/to/pyvenv/var/sensorhub-instance/development.db
). This puts your database neatly outside your project repository folder.Using Blueprints¶
Blueprints
are another neat Flask feature. They are helpful in managing related code. For instance, let's make a wild guess that eventually our project is going to have an admin interface in addition to the API itself. This means that we now have two distinct groups of views. We can make life easier for our future selves by connecting all of the API views to a blueprint. Using a blueprint affects how routes are defined. Each blueprint has a route prefix (e.g. /api/
). All routes with that prefix are handled by the associated blueprint. Inside the blueprint, routes are defined without the prefix (e.g. /sensor/
. When a request to /api/sensor/
comes in, Flask knows to let our API blueprint handle the rest of the routing (i.e. match with /sensor/
within the blueprint's routes). In order to achieve this, the very beginning of our
api.py
file will look like this:from flask import Blueprint
api_bp = Blueprint("api", __name__)
This blueprint needs to be registered inside the create_app function. Similarly to adding the command in the previous section, it's best to import app.py inside the function to avoid circular import issues. Drop these two lines inside the function.
from . import api
app.register_blueprint(api.api_bp)
We don't have any views yet, but this is a good basis for creating a strong independent API blueprint (not to be confused with
API Blueprint
which is a description language for APIs...) This file will eventually define routes to all of our resources. Avoiding Circular Imports¶
One glaring issue with Flask RESTful is its customized
api.url_for
method. The problem with the method is that in order to build a URL for a resource it needs the resource class as its first argument. This is a non-issue in single file applications, but becomes a big issue when resources are split into multiple files because they will need to import each other in order to be able to refer to the classes. If you try to do the imports in the typical way that makes the most sense (e.g. from sensorhub.resources.sensor import Sensor, SensorCollection
in the beginning of the module) you will run into a circular import problem. While there are some ways to work around this, a better solution exists: we don't have to use the customized
api.url_for
method. Instead we can use Flask's basic url_for
function. The first argument for this function is a string naming and endpoint to determine which URL will be generated. For a resource class the default endpoint name is the class name in lowercase. When using a blueprint, the endpoint name is prefixed with the blueprint name. As an example to turn this single-file safe URL generation that uses classesfrom sensorhub.api import api
from sensorhub.resources.sensor import SensorItem # NOTE: this is the problem line
href = api.url_for(SensorItem, sensor="uo-donkeysensor-1")
into one that uses endpoint names instead, avoiding circular import issues
from Flask import url_for
href = url_for("api.sensoritem", sensor="uo-donkeysensor-1")
Do note that you can't drop this example into the Python console because it requires app context to work. However if you use it within a view (resource class method) it always works (as views automatically have app context). So it will just work in your actual API code. In unit tests you need to take care to use the
with app.app_context()
statement.Making the Project Installable¶
If you try to run any tests with the current project directory structure, they will not work because you actually cannot import your application. In order to run tests you actually need to install your project so that it can be found from the virtual environment's path. This is the stage where virtualenvs become extremely useful - you don't want to be installing projects into your operating system's Python at this stage of development. Python packages are installed with setup scripts, i.e. a file called
setup.py
. This file contains information about the package like name, version, and dependencies. They look like this:from setuptools import find_packages, setup
setup(
name="sensorhub",
version="0.1.0",
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=[
"flask",
"flask-restful",
"flask-sqlalchemy",
"SQLAlchemy",
]
In this configuration, packages is the list of Python packages that belong to the project. We also want to include package data - this refers to files that are not Python modules such as static HTML files, JSON schemas, pictures etc. that we have in our static folder. In order for them to be included they also need to be listed in a file called
MANIFEST.in
. The following file would include everything in our static folder, and ignore all bytecode files:graft sensorhub/static global-exclude *.pyc
After creating this file, the project can be installed in editable mode so that you don't need to reinstall it whenever you make changes. You use pip to install it, and add the -e option. In the folder where your setup.py is:
pip install -e
The project is now installed and its packages can be imported from anywhere within the virtual environment.
Implementing Tests¶
Tests are largely implemented in the same way as before. The only thing that changes is a couple of imports, and how to create
fixtures
. The application is now imported from the virtual environments path instead of path relative to current working directory. Thus to import the app and its models:from sensorhub import create_app, db
from sensorhub.models import Location, Sensor, Deployment, Measurement
We're also just directly taking the database handle from the module, and use our fixture to create an app using test configuration. This configuration applies to the db handle we imported.
@pytest.fixture
def app():
db_fd, db_fname = tempfile.mkstemp()
config = {
"SQLALCHEMY_DATABASE_URI": "sqlite:///" + db_fname,
"TESTING": True
}
app = create_app(config)
with app.app_context():
db.create_all()
yield app
os.close(db_fd)
os.unlink(db_fname)
From now on all test cases should have
app
as their parameter. With these changes all tests we implemented previously will work.Give feedback on this content
Comments about this material