Home » Flask 教程 » Building websites in Python with Flask
  • 28
  • 08月

For some times now, I have been doing some projects in Python and some were web applications. Flask is a small framework to do exactly that and I have found it perfect for the job. It's really easy to use, fast, has good documentation and a good community.

This is the first post in a series dedicated to building websites with Python and more notably Flask. In this post, I will talk about setting up Flask with a database, using configuration environments, managing assets and deploying the app to production.

Contents:

[TOC]

First steps with Flask

I use pip to install Python modules and I would strongly recommend it (as well as using virtualenv).

Installing Flask is as easy as:

pip install Flask

Flask has an excellent quickstart tutorial so I will only do a quick overview of the basics.

As a framework, Flask is similar to Sinatra in Ruby or Slim in PHP. A main application object is instanciated and is used to map urls to functions.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()

In this example taken from Flask's quickstart tutorial, app is our application object and is used to map the hello_world() function to the path / . This is done using the @app.route() decorator.

app.run() starts the built-in web server on port 5000. Thus, your first Flask web app is available at http://localhost:5000 . Starts the webserver by calling the script:

python app.py

As stated earlier I would suggest you to read Flask's quickstart tutorial. Let's dive in another example:

from flask import Flask, render_template, request, redirect, url_for, abort, session

app = Flask(__name__)
app.config['SECRET_KEY'] = 'F34TF$($e34D';

@app.route('/')
def home():
    return render_template('index.html')

@app.route('/signup', methods=['POST'])
def signup():
    session['username'] = request.form['username']
    session['message'] = request.form['message']
    return redirect(url_for('message'))

@app.route('/message')
def message():
    if not 'username' in session:
        return abort(403)
    return render_template('message.html', username=session['username'],
                                           message=session['message'])

if __name__ == '__main__':
    app.run()

In this example users will enter their name and what they want to say on the first page. The data will be stored in the session and will be displayed on the /message page.

A really important concept in Flask is the request context. Flask uses thread-local objects, like request , session and others to represent elements of the current request. These objects are only available when a request context has been initialized, which is done by Flask when it receives an HTTP request.

Some observations:

  • app.config is a dict containing configuration parameters
  • @app.route() is by default limited to GET requests. Allowed HTTP methods of an action can be specified using the methods keyword arg.
  • url_for(route_name, **kwargs) should be used to generate urls for your handlers. It takes as first parameter the function name and as keyword args any needed parameters to generate the url.
  • redirect(url) creates an HTTP response with a redirect code and location
  • abort(http_code) is used to create error responses and stop the executing function.

In the signup() function, the request's data is accessed through the request object. request.form is a MultiDict (which acts like a normal dict for the most part) with all POST data, request.args is a MultiDict with GET params and request.values is a combination of both.

Flask is natively integrated with jinja2, a very good templating engine. Templates should be saved as .html files under the templates/ folder. The render_template(filename, **kwargs) function is a pretty straightforward method to render them.

index.html:

{% extends "layout.html" %}
{% block content %}
    <h1>Say something</h1>
    <form method="post" action="{{ url_for('signup') }}">
        <p><label>Username:</label> <input type="text" name="username" required></p>
        <p><label>Message:</label> <textarea name="message"></textarea></p>
        <p><button type="submit">Send</button></p>
    </form>
{% endblock %}

message.html:

{% extends "layout.html" %}
{% block content %}
    <h1>{{ username }} said:</h1>
    <p>
        {{ message }}
    </p>
    <a href="{{ url_for('home') }}">Say something else</a>
{% endblock %}

layout.html:

<!doctype html>
<html lang="en">
    <head>
        <title>Say somthing</title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
    </head>
    <body>
        {% block content %}{% endblock %}
    </body>
</html>

As you can see, I have used jinja's template inheritance to add a common layout to all of my templates.

I have also used the url_for() function to get the urls of files located in the static/ folder using the static route.

File organization and management script

In our example, our application is a single file. As you can guess, this will not suffice as it becomes larger.

I find a good approach is to treat the app as a python package. The package name will be the app name and the initialization of the Flask object is done in the __init__.py file.

The templates/ folder will be located inside the package's directory as well as any other files related to our application (eg. settings.py, models.py…).

example/
  __init__.py
  static/
    favicon.ico
  templates/
    index.html
    hello.html

Note: as the application grows larger, you should use Flask's Blueprints to organize your code as modules. I will cover this in another tutorial.

I also like to use the Flask-Script extension to manage my application from the command line. This extension provides built-in commands as well as a mechanism to define your own ones.

$ pip install Flask-Assets

I configure and run the extension from a manage.py file located outside the module's directory:

1
2
3
4
5
6
7
8
#!/usr/bin/env python
from flaskext.script import Manager, Shell, Server
from example import app

manager = Manager(app)
manager.add_command("runserver", Server())
manager.add_command("shell", Shell())
manager.run()

To start the server from the command line:

$ ./manage.py runserver

Using a database

There is no out of the box support for any database with Flask. Which is not a problem as there are many database libraries in Python. The most known and the one I prefer is SqlAlchemy. In addition to its excellent database toolkit, it features an awesome ORM.

Integrating SqlAlchemy with Flask cannot be simpler, thanks to the Flask-SqlAlchemy extension.

$ pip install Flask-SqlAlchemy

If it's your first time with SqlAlchemy, I would advice reading the ORM tutorial which is a very good start.

I like to initialize the extension and configure my models in a models.py file.

from flask_sqlalchemy import SQLAlchemy
from example import app

db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String)
    message = db.Column(db.String)

    def __init__(self, username, message):
        self.username = username
        self.message = message

We'll then need to add the database connection parameters to the configuration. For this example, we'll use sqlite. In __init__.py add this line after the previous app.config one:

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://example.db'

To initialize the database, I add a createdb command to the manage.py file. Add the following lines before manager.run():

@manager.command
def createdb():
    from example.models import db
    db.create_all()

Then run:

$ ./manage.py createdb

We can now modify our example to use the model, in __init__.py add the following lines after the creation of the app object:

# ...

from models import *

# ...

@app.route('/signup', methods=['POST'])
def signup():
    user = User(request.form['username'], request.form['message'])
    db.session.add(user)
    db.session.commit()
    return redirect(url_for('message', username=user.username))

@app.route('/message/<username>')
def message(username):
    user = User.query.filter_by(username=username).first_or_404()
    return render_template('message.html', username=user.username,
                                           message=user.message)

Instead of using the session we are now creating a User object, storing it into the database using db.session.add() and db.session.commit() (which is the standard SqlAlchemy way of doing it).

In the message() function I have added a username parameter which must be given through the url's path. We next perform a database query using User.query . Note the first_or_404() function which is provided by the flask extension and is a nice addition.

Configuration

As we've seen in the previous example, the configuration can be defined using the app.config dict. While this is the easier method, it is not the most practical one once you factor in configuration environments. Indeed, the configuration of your app will most of the time be different between your development environment and your production one.

The Flask documentation suggests a nice way of handling these configuration environments.

We'll store our configuration as attributes of plain old python objects in a file called settings.py. After reading the current environment form a system environment variable (EXAMPLE_ENV in our case) we'll load the config from the correct config object.

In the settings.py file as follow:

class Config(object):
    SECRET_KEY = 'secret key'

class ProdConfig(Config):
    SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example'

class DevConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite://example.db'
    SQLALCHEMY_ECHO = True

In our package's __init__.py :

import os

# ...

env = os.environ.get('EXAMPLE_ENV', 'prod')
app.config.from_object('example.settings.%sConfig' % env.capitalize())
app.config['ENV'] = env

Assets

With the rise of rich frontends and of alternative syntaxes to css and javascript, assets manaegement has become an important aspect of web apps.

We'll once again use a great extension, Flask-Assets, which is a wrapper for the webassets python library.

$ pip install Flask-Assets

I store my assets file in static/ , organizing them inside a css/ , js/ and vendor/ folders. Below you can see that I've added jquery and bootstrap in the vendor dir.

example/
  static/
    css/
      layout.less
    js/
      main.js
    vendor/
      bootstrap/
        css/
          bootstrap.css
        js/
          bootstrap.min.js
      jquery/
        jquery-1.7.2.min.js

With webassets, files are grouped as bundles. Each bundle can have custom filters (eg: transform less files to css). I define my bundles inside an assets.py file:

from flask_assets import Bundle

common_css = Bundle(
    'vendor/bootstrap/css/bootstrap.css',
    Bundle(
        'css/layout.less',
        filters='less'
    ),
    filters='cssmin', output='public/css/common.css')

common_js = Bundle(
    'vendor/jquery/jquery-1.7.2.min.js',
    'vendor/bootstrap/js/bootstrap.min.js',
    Bundle(
        'js/main.js',
        filters='closure_js'
    ),
    output='public/js/common.js')

Here I have defined 2 bundles, one for css files and one for js files. I've also use nested bundles to apply specific filters to some files.

To include the bundles in our pages, webassets provides some jinja2 helpers (which we should add to layout.html):

{% assets "common_css" %}
    <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}" />
{% endassets %}
{% assets "common_js" %}
    <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}

Now we'll need to configure the webassets environment in __init__.py :

from flask_assets import Environment
from webassets.loaders import PythonLoader as PythonAssetsLoader
import assets

# ...

assets_env = Environment(app)
assets_loader = PythonAssetsLoader(assets)
for name, bundle in assets_loader.load_bundles().iteritems():
    assets_env.register(name, bundle)

As you can see, I use webassets's PythonLoader to load bundles from the assets module and register each bundle into the environment.

You can add ASSETS_DEBUG=True in DevConfig to get debugging info. There are many other configuration parameters listed here. Param names should be prefixed with ASSETS_ and uppercased (eg. Environment.versions becomes ASSETS_VERSIONS ).

Finally, the Flask-Assets extension provides some command line utilities that we 'll need to register in the manage.py file:

from flask_assets import ManageAssets
from example import assets_env

# ...

manager.add_command("assets", ManageAssets(assets_env))

Available commands are listed in webassets documentation but the most important one is rebuild which regenerates all your bundle files:

$ ./manage.py assets rebuild

Deploying to production

Now that we have a fully working Flask application, we'll need to deploy it on a production machine. I like to use uWSGI + Nginx + Supervisor for my setup.

Note: this part assumes Ubuntu as your Linux distribution

Nginx acts as the frontend web server and will serve static files. uWSGI acts as the WSGI server which runs our flask app. Finally, I use supervisor to manage processes. I like to use Supervisor instead of init.d scripts as I often have other processes to manage.

$ sudo apt-get install nginx supervisor
$ pip install uwsgi

Configure an uWSGI app in /etc/uwsgi.ini :

[uwsgi]
socket = 127.0.0.1:3031
chdir = /path/to/my/app
module = example:app
env = EXAMPLE_ENV=prod

Add a server entry in Nginx in /etc/nginx/sites-enabled/example.conf :

server {
    listen 80;
    server_name example.com;
    root /path/to/my/app/example/static;

    location / {
        try_files $uri @uwsgi;
    }

    location @uwsgi {
            include uwsgi_params;
            uwsgi_pass 127.0.0.1:3031;
    }
}

Finally, configure Supervisor to manage the uWSGI process in /etc/supervisor/conf.d/example.conf :

[program:example]
command=/usr/local/bin/uwsgi --ini /etc/uwsgi.ini
autostart=true
autorestart=true
stopsignal=INT

And restart everything:

$ sudo /etc/init.d/nginx restart
$ sudo /etc/init.d/supervisor reload

Update: the next part in the series has been published: Getting bigger with Flask

Update 2: fixed the bugs and added database initialization


Origin: http://maximebf.com/blog/2012/10/building-websites-in-python-with-flask