writing your first web app using python and flask
DESCRIPTION
This tutorial will help you create your first webapp using Python and the Flask microframework. We will cover the following topics: * What is Flask and why would you use it * Rendering your first response * Handling forms and POST data * Using SQLAlchemy * Writing your first RESTful API * Holding open a connection to emit updates * Testing your app with py.test * Deploying to Redhat's Openshift platform Familiarity with Python is assumed. A passing familiarity with Javascript is recommended by not essential. https://github.com/danni/linux-conf-au-flask-tute http://mirror.linux.org.au/linux.conf.au/2014/Thursday/87-Writing_your_first_web_app_using_Python_and_Flask_-_Danielle_Madeley.mp4TRANSCRIPT
Writing your firstweb app usingPython and Flask
Danielle Madeley, Infoxchange blogs.gnome.org/danni dannipenguin
flickr.com/photos/kevinomara/7695277030
If you haven't already...git clone
git://github.com/danni/linux-conf-au-flask-tute
Follow the README to set up yourenvironment
What is Flask and why would you use itRendering your first responseHandling forms and POST dataUsing SQLAlchemyWriting your first RESTful APIHolding open a connection to emit updatesTesting your app with py.testDeploying to Redhat's Openshift platform
import cgi Just kidding!
Just what is Flask?
flickr.com/photos/markstos/3579118891
Rendering yourfirst response
#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__': app.run(debug=True)
#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)
@app.route('/')def index(): """Homepage"""
return "Hello, linux.conf.au"
if __name__ == '__main__': app.run(debug=True)
x â pythonÿenv�bin�activatex git checkout example÷ÎÏx pip install ÷r requirementsâtxtx python webapp�ÿÿinitÿÿâpy
from flask import redirect, render_template
@app.route('/a-redirect')def a_redirect(): """Redirect the user"""
return redirect(SOME_URL)
@app.route('/a-template')def a_template(): """Render a page using a Jinga2 template"""
return render_template('template.html')
By default: templates arelocated in the module'stemplates/ directory:
webapp/__init__.pytemplates/
template.html
x git checkout example÷ÎÐx pip install ÷r requirementsâtxtx python webapp�ÿÿinitÿÿâpy
handling querystrings
from flask import request
@app.route('/')def index(): """Return the name query argument"""
return request.args['name']
or doing itRESTfully
@app.route('/item/<int:pk>')def get_item(pk): """Return the item referred to by pk"""
# ...
?ou=Linux+Australia&ou=linux.conf.aurequestâargs�ëouë� �� ëLinux Australiaërequestâargsâgetlist�ëouë� �� �ëLinux Australiaëã ëlinuxâconfâauë� requestâargs�ënot presentë� raises KeyErrorrequestâargsâgetlist�ënot presentë� �� ��
handling forms andPOST data
from flask import request
@app.route('/')def index(): """Return the name POST value"""
return request.form['name']
or better still use aforms library
pip install Flask÷WTF
from flask import Flask, render_templatefrom flask.ext.wtf import Form
from wtforms import TextField
class RegoForm(Form): """A simple rego form"""
email = TextField('Email')
@app.route('/register', methods=('GET', 'POST'))def get_register(): """Handle the registration form"""
form = RegoForm()
if form.validate_on_submit(): return "Success"
return render_template('template.html', form=form)
if __name__ == '__main__': app.secret_key = 'THIS IS REALLY SECRET' app.run(debug=True)
from flask.ext.wtf import Form
from wtforms import TextField, validators
class RegoForm(Form): """A simple rego form"""
email = TextField('Email' validators=(validators.DataRequired(), validators.Email()))
<form method="post"> {{ form.hidden_tag() }} {# for CSRF.. important! #} {{ form.email.label }} {{ form.email }} {% if form.email.errors %} <ul> {% for error in form.email.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <input type="submit"></form>
SQL alchemy pip install Flask÷SQLAlchemy
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
class User(db.Model): """A user in my database"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
# ...
if __name__ == "__main__": app.config['SQLALCHEMY_DATABASE_URI'] = \ 'postgresql://username:password@localhost/myapp' app.run(debug=True)
user = db.session.query(User)\ .filter(User.user_id == user_id)\ .one()
+ migrations(with Alembic)
pip install Flask÷Migrate
Detour:
Flask-Script
from flask import Flaskfrom flask.ext.script import Manager
app = Flask(__name__)manager = Manager(app)
if __name__ == '__main__': manager.run()
x python ÿÿinitÿÿâpyusageä ÿÿinitÿÿâpy �÷h� �shellãrunserver� âââ
positional argumentsä �shellãrunserver� shell Runs a Python shell inside Flask application contextâ runserver Runs the Flask development server iâeâ appârun��
optional argumentsä ÷hã ÷÷help show this help message and exit
x python ÿÿinitÿÿâpy runserver � Running on httpä��ÏÐÕâÎâÎâÏäÓÎÎÎ� � Restarting with reloader
from flask import Flaskfrom flask.ext.migrate import Migrate, MigrateCommandfrom flask.ext.script import Managerfrom flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
manager = Manager(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)manager.add_command('db', MigrateCommand)
x python webapp�ÿÿinitÿÿâpy db initx git add migrations
x python webapp�ÿÿinitÿÿâpy db migratex git add migrations�versionsx python webapp�ÿÿinitÿÿâpy db upgrade
Tie it all together so far:
Models and Forms
from flask.ext.wtf import Form
from wtforms.ext.sqlalchemy.orm import model_form
class User(db.Model): """A user"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(256), unique=True)
UserForm = model_form(User, base_class=Form)
from flask.ext.wtf import Form
from wtforms import validatorsfrom wtforms.ext.sqlalchemy.orm import model_form
UserForm = model_form(User, base_class=Form, field_args={ 'email': { 'validators': [validators.Email()], },})
from flask import Flask, request, render_template, redirect, url_for
@app.route('/register', methods=('GET', 'POST'))def register(): """Register a new user"""
obj = User() form = UserForm(request.form, obj)
if form.validate_on_submit(): form.populate_obj(obj) db.session.add(obj) db.session.commit()
return redirect(url_for('register'))
return render_template('template.html', form=form, users=User.query.all())
Emitting events(and other streams)
Detour:
Generators
def fibonacci(): """Generate an infinite Fibonacci sequence""" a = 0 b = 1
while True: yield b c = a + b a = b b = c
for i in fibonacci(): print i
# WARNING: will never end!
def fibonacci(n=10): """Generate an n elements of the Fibonacci sequence""" a = 0 b = 1
for i in xrange(n): yield b c = a + b a = b b = c
for i in fibonacci(): print iÏÏÐÑÓÖÏÑÐÏÑÒÓÓ
We can use agenerator to emitthe response!
from flask import Response, stream_with_context
@app.route('/events/stream')def get_events(): """Return a stream of events"""
@stream_with_context def generate(): """ A generator that returns a single JSON-encoded event, followed by an empty line. """
while True: yield get_event()
return Response(generate(), mimetype='text/event-stream')
x git checkout example÷ÎÓx pip install ÷r requirementsâtxtx python webapp�ÿÿinitÿÿâpy runserver ÷÷threaded
But wait!
Flask will gracefully finish requests.One request will never finish.
from flask.ext.script import (Manager, Server as ServerCommand)
class Server(ServerCommand): def handle(self, *args, **kwargs): app.running = True
super(Server, self).handle(*args, **kwargs)
print "Shutting down" app.running = False
manager.add_command('runserver', Server)
from Queue import Queue
queue = Queue()
@stream_with_contextdef generate(): """ Yield JSON-encoded events """
while app.running: try: item = queue.get(timeout=1)
yield format_messages([item]) queue.task_done() # eat the queue item except Empty: pass
the other sidewarning: javascript ahead
<title>Event Stream</title> <ul id="messages"> {% for message in get_flashed_messages() %} <li>{{ message }}</li> {% endfor %} </ul>
<script src="/static/bower/jquery.js" type="text/javascript"></script><script src="/static/bower/jquery.eventsource.js" type="text/javascript"></script
<script type="text/javascript">
</script>
$(document).ready(function() { $.eventsource({ label: 'connect', dataType: 'json', url: '/events/stream', message: function(data) { $.each(data, function() { $('<li>', { text: this }).appendTo('#messages'); }); } })});
x git checkout example÷ÎÔx pip install ÷r requirementsâtxtx bower install � NâBâ httpä��bowerâio�x python webapp�ÿÿinitÿÿâpy collectstatic
If you're into websockets you can look atgithub.com/kennethreitz/flask-sockets
Testing withpy.test
tests/conftest.pyimport pytest
from webapp import (app as flask_app, db as flask_db)
@pytest.fixture(scope='session')def db(): """Set up the database"""
flask_app.config['TESTING'] = True flask_app.config['SQLALCHEMY_DATABASE_URI'] = ...
flask_db.drop_all() flask_db.create_all()
return flask_db
@pytest.fixture(scope='session')def app(db): """Set up the Flask test client"""
return flask_app.test_client()
tests/test_views.py"""app and db are available in the test scope"""
def test_index(app): """Test I can get the index page"""
rv = app.get('/')
print rv.data
assert '<title>' in rv.data
x git checkout example÷ÎÕx pip install ÷r requirementsâtxtx pyâtest tests�
deploying toOpenShift
To deploy OpenShift runs your top-levelsetup.py
To serve requests OpenShift runswsgi.application.application
www.openshift.com/developers/python
setup.pyimport os
from setuptools import setup, find_packages
PROJECT_ROOT = os.environ.get('OPENSHIFT_REPO_DIR', os.path.dirname(os.path.abspath(__file__)))
with open(os.path.join(PROJECT_ROOT, 'requirements.txt')) as file_: requirements = [req.strip() for req in file_.xreadlines()]
setup(name='example', version='0.0', author='Danielle Madeley', author_email='[email protected]', url='https://github.com/danni/linux-conf-au-flask-tute', description='Example deploy to OpenShift', install_requires=requirements, )
wsgi/application.pyimport osimport sys
# add local codesys.path.append(os.path.join(os.environ['OPENSHIFT_REPO_DIR']))
# initialise virtual environmentvirtenv = os.environ['OPENSHIFT_HOMEDIR'] + 'python-2.7/virtenv/'os.environ['PYTHON_EGG_CACHE'] = os.path.join(virtenv, 'lib/python2.7/site-packages'
virtualenv = os.path.join(virtenv, 'bin/activate_this.py')
try: execfile(virtualenv, dict(__file__=virtualenv))except IOError: pass
# import and configure WSGI applicationfrom webapp import app as application
app = Flask(__name__)
app.secret_key = os.environ.get('OPENSHIFT_SECRET_TOKEN', 'THIS IS REALLY SECRET')app.config['SQLALCHEMY_DATABASE_URI'] = \ os.environ.get('OPENSHIFT_POSTGRESQL_DB_URL', 'sqlite:///../app.db')
try: app.static_folder = os.path.join(os.environ['OPENSHIFT_REPO_DIR'], 'wsgi'except KeyError: pass
(assuming you've set up Openshift)x rhc app create example python÷ÐâÕ ÷÷no÷gitx git remote add rhc sshä��âââ�example÷âââ�£�git�exampleâgit�
x rhc cartridge add ÷c postgresql÷×âÐ ÷a example
x git push rhc ÷÷force example÷ÎÖämaster
setup.pymanage.pywebapp
__init__.pymanager.pymodels.pyviews.pycollectstatic.pytemplates
events.htmltemplate.html
Going FurtherGetting Bigger
class based viewsfrom flask.views import MethodView
class UserAPI(MethodView):
def get(self): users = User.query.all() ...
def post(self): user = User.from_form_data(request.form) ...
app.add_url_rule('/users/', view_func=UserAPI.as_view('users'))
blueprintsfrom flask import Blueprint, render_template
simple_page = Blueprint('simple_page', __name__, template_folder='templates')
@simple_page.route('/<page>')def show(page): return render_template('pages/%s.html' % page) </page>
blueprintsfrom flask import Flaskfrom module.simple_page import simple_page
app = Flask(__name__)app.register_blueprint(simple_page)
fin ;-)github.com/danni/linux-conf-au-flask-tute
blogs.gnome.org/danni dannipenguin
flickr.com/photos/mau3ry/3763640652