Home » Flask 教程 » Getting bigger with Flask
  • 28
  • 08月

Getting bigger with Flask

My last post about creating websites with Flask covered the steps to create a simple application. What happens when it grows bigger?

In this post I will take as example a common use case for a web app:

  • a public section (homepage, tour, signup, login)
  • a member only section (the app, user settings)
  • an api

Each member will have its own subdomain (ie: if my username is maximebf, I get the maximebf.example.com subdomain). This app will need input validation and background processing (eg: resizing the user's profile picture).

I'll assume the same file organization as I described in my previous post.

Contents:

[TOC]

Getting modular with Blueprints

Flask provides a feature called Blueprints which let your organize your app as modules. As always, the documentation about Blueprints is pretty comprehensive.

I like to create a modules folder in my application directory where all my module files will be stored. I create one python file per module.

example/
  modules/
    __init__.py
    public.py
    member.py
    api.py

To create a module, you initializes a Blueprint object which acts in the same way as the Flask object. Most importantly you can register handlers using the same route() decorator.

In the example/modules/public.py file:

from flask import Blueprint

blueprint = Blueprint('public', __name__)

@blueprint.route('/')
def home():
    return render_template('public/home.html')

Each blueprint can have its own templates folder. However, I don't like to do it that way and prefer to keep a single templates folder. I find it easier to have all my template files in the same directory. Thus, the public/home.html file would be located in example/templates/ .

I use the same logic for static files. Anyway, as I use webassets to manage them, using the static file feature from blueprints wouldn't be practical.

Don't forget to add the module name when building urls using url_for(). For example, the name of the url for our home() function would be public.home.

Blueprints need to be registered against a Flask object. The great thing about this is that you can specify at which endpoint using url_prefix and subdomain . In the example/init.py file:

from modules import public, member, api
app.register_blueprint(public)
app.register_blueprint(member, subdomain='<subdomain>')
app.register_blueprint(api, subdomain='api')

Note that there shouldn't be any handlers left in the __init__.py file. I think once you start using modules, all handlers should be located in modules.

Using this scheme, the public module is accessible from example.com/ , the member module from *.example.com/ and the api module from api.example.com/ .

Wildcard subdomains

As you may have noted, when I registered the member module against the Flask object, I used a dynamic parameter for the subdomain (the same way as with URLs). We can use this parameter to fetch the associated user using a combination of url processors and before request callbacks.

Here is a handy function to add subdomain support for any Flask or Blueprint object.

def add_subdomain_to_global(endpoint, values):
    g.subdomain = values.pop('subdomain', None)

def add_subdomain_to_url_params(endpoint, values):
    if not 'subdomain' in values:
        values['subdomain'] = g.subdomain

def add_subdomain_support(app):
    app.url_value_preprocessor(add_subdomain_to_global)
    app.url_defaults(add_subdomain_to_url_params)

You can then use a before_request callback to process the subdomain:

add_subdomain_support(blueprint)

@blueprint.before_request
def add_user_to_global():
    g.user = None
    if g.subdomain:
        g.user = User.query.filter_by(username=g.subdomain).first_or_404()

Handling html forms

WTForms is one of the best Python solution to manage html forms. The Flask documentation has a great section about integrating and using WTForms with your app. I like to define my forms at the top of the module files.

If you are using the Bootstrap CSS framework, here is a Jinja2 macro to generate Bootstrap compatible form fields:

{% macro form_field(field) -%}
    {% set with_label = kwargs.pop('with_label', False) %}
    {% set placeholder = '' %}
    {% if not with_label %}
        {% set placeholder = field.label.text %}
    {% endif %}
    <div class="control-group {% if field.errors %}error{% endif %}">
        {% if with_label %}
            <label for="{{ field.id }}" class="control-label">
                {{ field.label.text }}{% if field.flags.required %} *{% endif %}:
            </label>
        {% endif %}
        <div class="controls">
            {% set class_ = kwargs.pop('class_', '') %}
            {% if field.flags.required %}
                {% set class_ = class_ + ' required' %}
            {% endif %}
            {% if field.type == 'BooleanField' %}
                <label class="checkbox">
                    {{ field(class_=class_, **kwargs) }}
                    {{ field.label.text|safe }}
                </label>
            {% else %}
                {% if field.type in ('TextField', 'TextAreaField', 'PasswordField') %}
                    {% set class_ = class_ + ' input-xlarge' %}
                {% elif field.type == 'FileField' %}
                    {% set class_ = class_ + ' input-file' %}
                {% endif %}
                {{ field(class_=class_, placeholder=placeholder, **kwargs) }}
            {% endif %}
            {% if field.errors %}
                <span class="error help-inline">{{ field.errors|join(', ') }}</span>
            {% endif %}
            {% if field.description %}
                <p class="help-block">{{ field.description|safe }}</p>
            {% endif %}
        </div>
    </div>
{%- endmacro %}

I like to have a helpers.html file in my templates/ folder with some useful macros (like the one above). To import the macros use:

{% from "helpers.html" import form_field %}

Background processing with Celery

Any task which needs a bit of processing should be queued and processed as a background job. Celery is a great Python library which makes launching background tasks really easy.

I create a tasks.py file in the application directory where Celery will be initialized and all my delayed functions will be located.

from example import app
from celery import Celery

celery = Celery('example.tasks')
celery.conf.update(app.config)

@celery.task(ignore_result=True)
def resize_uploaded_image(filename, w, h):
    # ...

I find the easiest way to get Celery running is using Redis as the backend. In the settings.py file:

class Config(object):
    # ...
    BROKER_URL = 'redis://localhost:6379/0'

Launching a background job is done as described in the celery doc:

from example.tasks import resize_uploaded_image

# ...

@blueprint.route('/upload', methods=['POST'])
def upload_image():
    # ...
    resize_uploaded_image.delay(filename, 100, 100)

Finally, you'll need to start the worker using the celery CLI utility:

celery worker -A example.tasks -l info

This is where supervisor comes handy (see previous post) as we can add a new entry to automatically start the celery process:

[program:example_worker]
directory=/path/to/my/app
command=celery worker -A example.tasks -l info
autostart=true
autorestart=true

Origin: http://maximebf.com/blog/2012/11/getting-bigger-with-flask/