Author: | Andrew Montalenti |
---|---|
Date: | 2013-01-27 |
How this was made
This document was created using Docutils/reStructuredText and S5.
Rapid Web Prototyping with lightweight tools: a tour through "inside out" web development with Bootstrap, jQuery, Jinja2, Flask, and MongoDB.
Me: I've been using Python for >10 years. I use Python full-time, and have for the last 4 years.
Startup: I'm co-founder/CTO of Parse.ly, a tech startup in the digital media space.
E-mail me: andrew@parsely.com
Follow me on Twitter: amontalenti
Connect on LinkedIn: http://linkedin.com/in/andrewmontalenti
What do we do?
The main requirements for this course are:
Already done with these steps? Skip ahead!
If you aren't familiar with the command-line, programming text editors, UNIX, and/or don't have a good comfort level with basic usage of Python/Git already, then you can take the "beginner track" in this course.
Rather than setting up your local computer, you will simply follow along what I'm doing on the screen, and optionally connect to a server I have set up where you can experiment with an IPython Notebook and CodeMirror HTML Editor.
You can also download the code to follow along:
Open terminal and...
run python -V and make sure you're on Python 2.7.x.
run git --version to make sure you have git 1.7/1.8 installed.
A recent Python version (2.7.3) can be installed from Python.org.
A recent Git version (1.8.1) can be downloaded from git-scm.com.
This course assumes you can walk around the command line a bit.
A quick cheat sheet:
- ls: list files in current directory
- pwd: print working directory path
- cd <path>: change directory
- mkdir <path>: create a directory
- cat <file>: show contents of file
- nano <file>: open file in nano text editor
Make a work area:
mkdir ~/repos
Clone the code respository:
git clone git://github.com/amontalenti/rapid-web.git cd rapid-web
Inspect the tags:
git tag -l
Look at Github web interface. Feel free to fork!
All the changes from the initial check-in to the last on Github.
https://github.com/amontalenti/rapid-web/compare/v0.2-static...v2.0-fin
easy_install command may not be available in some borked Python versions on Linux and OS X.
Try easy_install --version to check.
If not available, use this script:
curl -O http://python-distribute.org/distribute_setup.py python distribute_setup.py
Run this virtualenv setup:
$ sudo easy_install pip $ sudo pip install virtualenv
Then:
$ cd rapid-web $ virtualenv rapid-env
This will create a self-contained Python installation for use with this tutorial.
One of the first Python development tools I'll use in hour 2 is IPython.
It lets us test code at the command-line easily.
Prototyping code at the command-line is one of the core ways to do effective prototyping beyond the HTML / CSS / JavaScript phase.
You should now have a virtualenv folder called rapid-env. For convenience, let's make it easy to activate:
$ ln -s rapid-env/bin/activate
Activate it with the "magic incantation":
$ source activate
And then, install IPython:
(rapid-env)$ pip install ipython ... lots of output ... (rapid-env)$ ipython -V 0.13.1
Install the requirements with pip:
$ cat requirements.txt ipython Flask $ pip install -r requirements.txt ...
Then, confirm that you can import all the libraries:
$ ipython >>> import flask >>> import jinja2 >>> import werkzeug >>> <CTRL+D> Do you really want to exit ([y]/n)? y $
If you install some optional requirements, you can get:
These are in dev-requirements.txt, which you can install with pip:
$ cat dev-requirements.txt # for live code updates livereload # for ipython notebook tornado pyzmq $ pip install -r dev-requirements.txt ... $ ipython notebook <CTRL+C to quit> $ livereload -p 8000 <CTRL+C to quit>
To actually use LiveReload, you need a browser extension for chrome which can be downloaded here:
https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei
We're going to ssh into a remote server in the final hour of the course for deployment.
To do this, we're going to need to add your public key to the server's list of "authorized keys".
If you are already a Github user or remotely manage servers with SSH, then you probably don't need to generate a new public key, but I've included these instructions here for those of you who don't already have public keys.
Try:
cat ~/.ssh/id_*.pub
Do you get output like:
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQE...
If so, you do already have a public key.
SSH has a small configuration file at ~/.ssh/config that allows you to specify hostnames that ssh will use.
Upon connecting to a server, ssh looks for an identity file for public key authentication. This is typically ~/.ssh/id_rsa.
This private key has a matching public key, which is typically ~/.ssh/id_rsa.pub and must be listed in the remote host's ~/.ssh/authorized_keys file.
ssh-keygen can create the public/private key for you. Then you need to share the public key with me.
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/user1/.ssh/id_rsa):
Your identification has been saved in /home/user1/.ssh/id_rsa.
Your public key has been saved in /home/user1/.ssh/id_rsa.pub.
The key fingerprint is:
43:55:f0:cc:1a:9f:ff:2e:3a:a8:94:8c:f1:62:d3:b1 user1@hacknode
...
Edit the file:
$ nano ~/.ssh/config
And insert contents:
Host hacknode User shared HostName hacknode.alephpoint.com
Then:
$ ssh hacknode
It'll prompt you for a password right, but that's OK -- just CTRL+C to abort.
On to the main course!
Thesis: the most important skill that a modern web developer can have today is prototyping.
Most web developers lack this skill due to a number of biases:
Let's take each of these in turn.
Myth: The most interesting problems in computing are algorithmic and backend systems oriented: e.g. data structures, natural language processing, operating systems, distributed systems, cryptography.
Reality: These are simply the most interesting problems to introverted CS PhDs. The most widely used software is not solving fundamental computing problems (think Twitter, Facebook, GMail, Reddit) but is instead solving user experience problems.
Myth: Only a trained graphic designer can create usable and functional user interfaces.
Reality: Anyone can create these interfaces; a skilled designer will promote these interfaces from the kind you merely use daily to the kind you share excitedly to friends.
Myth: The purpose of a web framework is to unify web technologies with backend (database) technologies, specifically a SQL database. Building a web app consists of building a SQL model, then building the interfaces on that model.
Reality: SQL isn't necessary in the early stages of a project; it may not be necessary at all during the entire lifetime of the project. Traditional web frameworks focus on the wrong thing.
Myth: The web requires knowledge of a slew of complex technologies: a backend programming language, a database query language, a templating language, JavaScript, HTML, and CSS. That's messy; I prefer to simply code in Java, Ruby, Python, etc.
Reality: This is partially true. The web is messy, but all that's necessary to build a web app is some basic knowledge of JavaScript and HTML. Much of the rest can be abstracted via modern toolkits like jQuery and Bootstrap. You also need a way to render that JavaScript/HTML code, but this isn't as tough as it seems.
Together, all of these biases form a general software engineering "backend bias" that I observe in the real world.
The typical Python software engineer has no problem with:
This is a kind of "comfort zone" for typical programmers.
Same engineers exhibit a real fear when confronted with:
I'm not really concerned with why this split exists, but I definitely observe it.
"Premature optimization is the root of all evil."
Don't overcomplicate your code with optimizations before you've measured whether those optimizations are actually necessary for acceptable runtime performance.
"Premature backend is the root of all evil."
Don't start to build the backend of your web app until you've determined what user experience your application will enable.
Do you think backends deserve to be built first? I'd like opinions from the class!
Now that you have a sense of the theory behind this course, I'll take you through three phases of rapid web prototyping:
Traditional software process:
Problem: between steps 1 and 9, MONTHS can pass.
Related problem: when building fundamentally new & innovative products, step 9 (feedback from real users) is the most important.
Can we skip from step 1 to step 9?
Yes: this is the essence of "rapid web prototyping".
We need to fake a test user into thinking a working system exists.
Idea: "Reddit for clickstreams!"
Reddit is cool, but the explicit "voting" process is annoying.
People don't mind submitting links, but who wants to take time to upvote/downvote them?
How about implicit voting based on users clicking on or re-submitting the same article?
Insight: click is "implicit upvote"; re-submit is "explicit upvote". No vote buttons necessary!
Do you start building a database? Frontpage, Article, and Link classes w/ ORM bindings? Hell, no!
Do you start researching high-concurrency web frameworks for your millions of potential users? Screw that!
Do you write a really detailed requirements document and complicated technical architecture? What good will that do!?
Let's prototype this idea.
Even before diving into code, let's think about how to sketch out this idea using the old pen and paper.
Quick discussion about wireframes:
Answer key questions:
You have 5 minutes. Then we'll share!
In my version of this wireframe, I have three main screens:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rapid News Static</title>
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<!-- fun stuff -->
</div>
<script src="js/main.js"></script>
</body>
</html>
An HTML document is like a little "envelope for your site."
In <head>, you describe the document itself. The <title> of the document, some metadata about the document, and links to relevant to stylesheets (CSS).
In the <body>, you put the "main entree": the content itself, or the site structural elements.
At the bottom of the <body>, you install <script> tags for any JavaScript you want to make the page behave in a dynamic way.
There are a few core HTML elements that our framework will use (and we'll describe later), but you should know a couple things about HTML / CSS first.
First, any element can have a name (id) and any number of styles (class). This will be important later.
<div id="container" class="on">
Rapid Web <span id="course" class="smallcaps">Prototyping</span>
</div>
Second, though there are many kinds of elements for different browser interface components, there are two special kinds of HTML elements with nearly limitless flexibility: <div> and <span>.
The div element creates a "block" element, which means, it is sized like a rectangle inside the browser.
Used to mark off "divisions" of your site page, e.g. distinguish a "header area" from the "content area" from the "footer area".
div elements can contain other div elements, creating a parent/child site structure.
The span tag creates an "inline" element, which means, it's used to wrap around text and images.
This is typically used for things like labels, in-text annotations, captions, etc.
Importantly, a span cannot contain a block element (like a div), but can be contained by one.
On their own, div and span are "meaningless containers" other than the above description.
They are therefore typically the tool used for all your CSS styling. And with the framework we are using, they are relied upon heavily, as well.
<html>
<head>
<link rel="stylesheet" href="css/lib/bootstrap.css">
<link rel="stylesheet" href="css/lib/bootstrap-responsive.css">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<!-- <bootstrap> -->
<!-- </bootstrap> -->
</div>
<script src="js/lib/jquery.js"></script>
<script src="js/lib/bootstrap.js"></script>
<script src="js/main.js"></script>
</body>
</html>
Smooths out the differences between different browser default HTML / CSS styling.
Provides a "grid" and "layout" system.
Supports responsive design approaches (auto scale down for mobile/tablet).
Improves typically-used HTML elements with some reasoanble default stylings.
Adds other commonly-used UI components that are "missing" from HTML.
Richer interactive components that require JavaScript.
We'll start with the navigation area and header.
We'll then add a simple listing of fake links in a table.
<div class="navbar">
<div class="navbar-inner">
<a class="brand hidden-phone">Rapid News</a>
<a class="brand visible-phone">RN</a>
<ul class="nav">
<li class="active"><a href="#">Links</a></li>
<li><a href="#">Submit</a></li>
</ul>
<form class="navbar-search pull-right hidden-phone" action="/search">
<small><i class="icon-search"></i> Search</small>
<input name="query" type="text" class="search-query" placeholder="">
</form>
</div>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Score</th> <th>Link</th> <th>Published</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="label label-important">150</span></td>
<td><a href="#">The Meditations of Marcus Aurelius</a></td>
<td><span class="label">3 hours ago</span></td>
</tr>
...
</tbody>
</table>
HTML isn't that hard.
With Bootstrap, you don't need to be a designer to get from wireframe to clickable.
Problems:
Enter jQuery.
Let's play in the Chrome Inspector console with the jQuery API.
function animateRows() {
// simple animation to fade in all but the top story
$("tbody tr").each(function(i, row) {
if (i === 0) {
// skip 1st row
return;
}
// capture current row
var elm = $(row);
// schedule it to fade in
setTimeout(function() {
elm.fadeIn();
}, i * 500);
});
};
// module.js
(function() {
// anonymous function creates namespace
// prevents global leakage
function myPrivateFunction() {
// private function created in namespace
};
function myPublicFunction() {
var elements = myPrivateFunction();
elements.each(function() { ... } );
// public function must be exported
};
// export as RAPID.myPublicFunction
RAPID.myPublicFunction = myPublicFunction;
})();
Use browser cookie to detect whether user has visited before.
Show a modal dialog on first visit to explain concept, and then hide it on future visits.
When first prototyping, the easiest approach is "pure static" -- just open your HTML file in your browser.
This has some limitations though:
Here is our first "Python backend".
A simple HTTP web server built into Python itself.
Just run:
cd static
python -m SimpleHTTPServer
Now open http://localhost:8000 in your browser.
<div id="first-visit-dialog" class="modal hide fade in">
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>Welcome to Rapid News!</h3>
</div>
<div class="modal-body">
<h4>Rapid News connects you with the latest links</h4>
<p>Links are prioritized on this page based on clicks and
submits. Simply click a link or submit a story and you're
instantly a part of the community.</p>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Close</a>
</div>
</div>
function showFirstVisitDialog() {
var cookie = RAPID.readCookie("visited");
if (cookie === "true") {
// do nothing, user has visited before
return;
}
var modal = $("#first-visit-dialog");
modal.on("hide", function() {
RAPID.createCookie("visited", "true", 30);
});
modal.modal();
};
Console and state.
http://hndroidapi.appspot.com
/best/format/json/page/
?appid=RAPID&
callback=
var apiroot = "http://hndroidapi.appspot.com";
var path = "/best/format/json/page/";
var params = "?appid=RAPID&callback=?";
var url = [apiroot, path, params].join("");
$.getJSON(url, function(data) {
$.each(data.items, function(i, item) {
console.log(item.title);
});
console.dir(data);
});
$.getJSON(url, function(data) {
var rows = $("table tr");
$.each(data.items, function(i, item) {
var row = rows.get(i+1);
if (typeof row !== "undefined") {
row = $(row);
var score = row.find("span.label:first");
var pubdate = row.find("span.label:last");
var link = row.find("a");
link.attr("href", item.url);
link.html(item.title);
score.html(item.score.replace(" points", ""));
pubdate.html(item.time);
}
});
});
Copypasta time!
<html>
<head>
<meta charset="utf-8">
<title>RN: Submit News</title>
<!-- ... -->
</head>
<body>
<div class="container">
<!-- <bootstrap> -->
<!-- </bootstrap> -->
</div>
<script src="js/lib/jquery.js"></script>
<script src="js/lib/bootstrap.js"></script>
<script>window.RAPID = {};</script>
<script src="js/submit.js"></script>
</body>
</html>
On main page:
<ul class="nav">
<li><a class="active" href="#">Links</a></li>
<li><a href="/submit.html">Submit</a></li>
</ul>
On submit page:
<ul class="nav">
<li><a href="/">Links</a></li>
<li class="active"><a href="#">Submit</a></li>
</ul>
<div id="submit-form">
<form action="/new">
<fieldset>
<legend>Submit some news!</legend>
<label>Link</label>
<input type="text" placeholder="http://...">
<label>Title</label>
<input type="text" placeholder="news headline or description">
<div class="control-group">
<button type="submit" class="btn">Submit!</button>
</div>
</fieldset>
</form>
</div>
Two pages: index.html and submit.html.
Using several Bootstrap components: navigation, table, form, modal.
JSON-P API calls to some fake data and jQuery for element manipulation.
Still no backend built.
Can be used to gather useful user feedback.
No server means no way to handle the submit form.
No way to track clicks (no link redirector).
No real scoring algorithm yet (data faked from HN).
Nothing! With very little code, we're providing a clickable UI.
De-risking some of our core assumptions about the product.
However, we can already see some code duplications (header/footer, nav).
Starting to hit the limits of no backend.
Time for Python to save the day!
Let's take a 5m break to answer questions / reflect a bit.
from werkzeug.wrappers import Request, Response
@Request.application
def app(request):
print request.path
print request.headers
return Response("hello, world!")
from werkzeug.serving import run_simple
run_simple("localhost", 4000, app)
from werkzeug.wrappers import Request, Response
from werkzeug.debug import DebuggedApplication
@Request.application
def app(request):
raise ValueError("testing debugger")
return Response("hello, world!")
app = DebuggedApplication(app, evalex=True)
from werkzeug.serving import run_simple
run_simple("localhost", 4000, app)
>>> request.headers
EnvironHeaders([('Cookie', 'csrftoken=ETXzOTz6zqbQYt0o...
>>> request.headers.keys()
['Cookie', 'Content-Length', 'Accept-Charset', 'User-Agent',
'Connection', 'Host', 'Cache-Control', 'Accept', 'Accept-Language',
'Content-Type', 'Accept-Encoding']
>>> request.headers["User-Agent"]
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.17 (KHTML, like Gecko)'
'Chrome/24.0.1312.68 Safari/537.17'
We can look at both sides of this request to really understand it.
To do anything "dynamic" in response to user requests.
For our Rapid News app, we need the server to:
Remember copypasta?
We want to avoid duplicating code between index.html and submit.html. As this app grows, we may add new pages, and we'd like to maintain a common look-and-feel (template heirarchy).
We want to render pages "dynamically" using data we've stored on the server (control flow and interpolation).
We want to enable HTML code re-use within pages (macros).
from jinja2 import Template
tmpl = Template(u'''<table>
<tr>
<td><strong>Number</strong></td> <td><strong>Square</strong></td>
</tr>
{%- for item in rows %}
<tr>
<td>{{ item.number }}</td> <td>{{ item.square }}</td>
</tr>
{%- endfor %}
<table>
''')
data = [{"number": number, "square": number*number}
for number in range(10)]
print tmpl.render(rows=data)
<table>
<tr>
<td><strong>Number</strong></td> <td><strong>Square</strong></td>
</tr>
...
<tr>
<td>3</td> <td>9</td>
</tr>
<tr>
<td>4</td> <td>16</td>
</tr>
...
<tr>
<td>9</td> <td>81</td>
</tr>
<table>
import os
import sys
import json
from jinja2 import Environment, FileSystemLoader
args = sys.argv
env = Environment(loader=FileSystemLoader(os.getcwd()))
data = json.load(open(args[2]))
print env.get_template(args[1]).render(data)
<table>
<tr>
<td><strong>Number</strong></td>
<td><strong>Square</strong></td>
</tr>
{%- for item in rows %}
<tr>
<td>{{ item.number }}</td>
<td>{{ item.square }}</td>
</tr>
{%- endfor %}
<table>
Saved in squares.jinja2.html.
$ python render.py squares.jinja2.html
{"rows": [{"number": 3, "square": 9}]}
<table>
<tr>
<td><strong>Number</strong></td>
<td><strong>Square</strong></td>
</tr>
<tr>
<td>3</td>
<td>9</td>
</tr>
</table>
def top_articles():
return []
def search_articles(query):
return []
def insert_article(article):
return False
from flask import Flask, render_template
from rapid import top_articles
app = Flask(__name__)
@app.route('/')
def index():
articles = top_articles()
return render_template('index.jinja2.html',
rows=articles)
if __name__ == "__main__":
app.run(debug=True)
In plain Python code, you tend to avoid global state like the plague.
In web applications, there is some implicit global state: the currently running "application", and the currently-being-handled "request".
Flask makes dealing with these easier than it would otherwise be.
app = Flask(__name__)
# the "application context"
... and...
from flask import request
@app.route('/')
def index():
# the "request context"
print request.headers
Core Idea: your Python functions / classes get "bound to a context".
Flask calls your code and sets appropriate shared state (e.g. current request via flask.request and the current session via flask.session).
You can also share arbitrary data in-process via flask.g (global).
In this way, Flask makes coupling between your code and the web server very explicit.
(Insight: Flask can create a "thin web layer" for plain Python code.)
URL Routing lets you bind HTTP paths and arguments to Python functions easily.
This is the "design of your URLs".
Let's look at an example of Flickr's URL design.
@app.route("/explore")
def explore_photos():
pass
@app.route("/photos")
def most_recent_photos():
pass
@app.route("/photos/<username>")
def user_photos(username):
pass
@app.route("/photos/<username>/<int:photo_id>")
def photo_detail(username, photo_id):
pass
@app.route('/')
def index():
pass
@app.route('/search/<query>')
def search(query):
pass
@app.route('/submit', methods=["GET", "POST"])
def submit():
pass
{# example layout.html #}
{# header #}
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{# /header #}
{% block body %}
{% endblock %}
{# footer #}
</body>
</html>
{# /footer #}
{# example index.html #}
{% extends 'layout.html' %}
{% block title %}Latest News{% endblock %}
{% block body %}
<table>
<thead>
...
</thead>
<tbody>
....
</tbody>
</table>
{% endblock %}
{# example submit.html #}
{% extends 'layout.html' %}
{% block title %}Submit News{% endblock %}
{% block body %}
<form>
<fieldset>
...
</fieldset>
</form>
{% endblock %}
$ cd templates
$ python render.py index.jinja2.html data.json
<html>
...
<table>
...
</html>
$ python render.py submit.jinja2.html data.json
<html>
...
<form>
...
</html>
In the render.py calls from before, data.json was a file with an empty JSON object.
{}
We can populate variables in here to create a "template context".
{"rows":
[
{"title": "Google",
"score": 150,
"link": "http://google.com"},
{"title": "Yahoo",
"score": 75,
"link": "http://yahoo.com"},
{"title": "Bing",
"score": 50,
"link": "http://bing.com"}
]
}
{% for row in rows %}
<tr>
<td>{{ row.score }}</td>
<td><a href="{{ row.link }}">{{ row.title }}</a></td>
<td>just now</td>
</tr>
{% endfor %}
$ python render.py index.jinja2.html articles.json
<html>
...
<body>
...
<tr>
<td>150</td>
<td><a href="http://google.com">Google</a></td>
<td>just now</td>
</tr>
...
</body>
</html>
from flask import render_template, Flask
app = Flask(__name__)
def top_articles():
articles = [
{"title": "Google", "score": 150, "link": "http://google.com"},
{"title": "Yahoo", "score": 75, "link": "http://yahoo.com"},
{"title": "Bing", "score": 50, "link": "http://bing.com"}
]
return articles
@app.route('/')
def index():
articles = top_articles()
return render_template("index.jinja2.html", rows=articles)
if __name__ == "__main__":
app.run(debug=True)
Backend:
Frontend:
Browser Request
--> WSGI Server
--> Flask App Context
--> View Function
--> Request Context
--> Python Code / Data Access
--> Template Context
--> Render Template
--> Response to Browser
Browser Response Parsing
--> Download & Parse CSS / JavaScript
--> Render DOM
--> Execute JavaScript
--> Register Event Handlers
--> Remote Requests (AJAX)
--> Dynamic Element Modification
--> Full Page Loaded
This request/response lifecycle is what makes web programming a little complex.
Paradox of choice re: where to put your logic.
Should core logic be in the browser (JavaScript), templates (Jinja2), in the request context (Flask) or just on the server (plain Python)?
The answer is, "it depends".
There has been a bit of a craze recently about "single-page web apps".
The idea is that for many web apps, almost all of the application logic can live in the browser.
The server only speaks an API (HTTP/JSON) and does not do things like template rendering.
Proponents of this approach say that it makes the applications more performant and unifies the codebase (mostly JavaScript).
JavaScript interpreters in modern browsers are fast enough for this now, whereas in e.g. 2004-2008, this would have been infeasible.
Multi-page apps tend to be more "web-friendly".
They also tend to be simpler to implement and debug.
Easy to selectively use single-page app techniques in a multi-page app.
Original "AJAX" craze was about this.
Browser Request to '/'
--> Flask Renders Static HTML
--> Flask Returns Static JavaScript Application
Browser Response
--> jQuery API call to /frontpage.json
--> New Flask Request
--> Python Logic to get top articles
--> Data Rendered as JSON
--> API data used to template/render client-side
User Sees Front Page
User Clicks "Submit"
--> JavaScript alters DOM
User Sees Submit Form
User Submits New Article
--> jQuery API call to /submit.json for validation
User Sees Validation Errors or Success
Browser Request to '/'
--> New Flask Request
--> Python Logic to get Top Articles
--> New Template Context with Data
--> Template Rendered with Jinja2
Browser Response
User Sees Front Page
User Clicks "Submit"
--> New Flask Request
--> New (Empty) Template Context for Submission Form
--> Template Rendered with Jinja2
User Sees Submit Form
User Submits New Article
--> New Flask Request
--> Python Form Validation Logic
--> New Template Context with Errors (or Empty)
--> Template Rendered with Jinja2
User Sees Validation Errors or Success
Let's take some time to go from v1.0-app to v1.1-jinja.
{%- macro link_tag(location) -%}
<link rel="stylesheet" href="static/css/{{ location }}.css">
{%- endmacro -%}
{%- macro script_tag(location) -%}
<script src="static/js/{{ location }}.js"></script>
{%- endmacro -%}
{# layout.jinja2.html #}
{% from 'util.jinja2.html' import link_tag, script_tag %}
...
<head>
<title>(RN) {% block title %}{% endblock %}</title>
{{ link_tag('lib/bootstrap') }}
{{ link_tag('lib/bootstrap-responsive') }}
{% block css %}
{% endblock %}
</head>
<body>
...
{% from 'util.jinja2.html' import link_tag, script_tag %}
...
{{ script_tag('lib/jquery') }}
{{ script_tag('lib/bootstrap') }}
<script>window.RAPID = {};</script>
{% block js %}
{% endblock %}
app = Flask(__name__,
static_folder="../static",
static_url_path="/static")
Will now look in "../static" and serve all static files there under "/static" URL, thus matching our macros.
{% extends 'layout.jinja2.html' %}
{% block title %}My Page{% endblock %}
{% block css %}
{{ link_tag('my-page') }}
{% endblock %}
{% block body %}
<div class="my-page"></div>
{% endblock %}
{% block js %}
{{ script_tag('my-page') }}
{% endblock %}
One of the original limitations of our Rapid News "fake" prototype is that the "Submit" page wasn't functional.
You might ask: why couldn't I implement that page 100% client-side?
Technically, you could, but there are a slew of reasons you don't want to do so.
All user data must be validated server-side. Optionally, can "convenience check" on client side.
JavaScript code must run under the "hostile environment" assumption that an attacker can change any aspect of DOM, functions, classes, etc.
Where dynamism is needed, it's preferable to do HTTP/JSON requests to server via XMLHTTPRequest or JSON-P.
Now that we understand why we need the server to validate data coming from the user, let's make our original "Submit" form server-enabled.
<div id="submit-form">
<form method="POST">
<fieldset>
<!-- ... -->
</fieldset>
</form>
</div>
<div class="control-group ">
<label class="control-label" for="link">Link</label>
<div class="controls">
<input type="text"
name="link" id="link"
value=""
placeholder="http://...">
</div>
</div>
<div class="control-group ">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input type="text"
name="title" id="title"
value=""
placeholder="headline or description">
</div>
</div>
{% macro input(name, desc, type='text', placeholder=None) -%}
<div class="control-group {{ error(name) }}">
<label class="control-label" for="{{ name }}">{{ desc }}</label>
<div class="controls">
<input type="{{ type }}"
name="{{ name }}" id="{{ name }}"
value="{{ request.form[name] }}"
placeholder="{{ placeholder }}">
{{ errorhelp(name) }}
</div>
</div>
{%- endmacro %}
{% macro error(name) -%}
{% if errors and errors[name] -%}
error
{% endif -%}
{% endmacro -%}
{% macro errorhelp(name) -%}
{% if errors and errors[name] -%}
<span class="help-inline">{{ errors[name] }}</span>
{% endif -%}
{% endmacro -%}
{% macro button(name) -%}
<button type="submit" class="btn btn-primary">{{ name }}</button>
{%- endmacro %}
<div id="submit-form">
<h2>Submit a new article!</h2>
<form method="POST">
<fieldset>
{{ input("link", "Link", placeholder="http://...") }}
{{ input("title", "Title", placeholder="headline or description") }}
<div class="form-actions">
{{ button("Submit") }}
</div>
</fieldset>
</form>
</div>
def validate_submission(params):
errors = {}
def err(id, msg):
errors[id] = msg
title = params["title"].strip()
if len(title) < 2:
err("title", "title must be > 2 characters")
if len(title) > 150:
err("title", "title may not be > 150 characters")
link = params["link"].strip()
try:
opened = urlopen(link)
link = opened.geturl()
except (URLError, ValueError):
err("link", "link could not be reached")
if len(errors) > 0:
return (False, errors)
else:
return (True, errors)
def do_submit():
form = request.form
submission = dict(
title=form["title"],
link=form["link"]
)
valid, errors = validate_submission(submission)
if valid:
article = insert_article(submission)
return render_template("success.jinja2.html",
page_submit="active")
else:
return render_template('submit.jinja2.html',
page_submit="active",
errors=errors)
Filters, like macros, are a form of code re-use in your templates.
Unlike macros, they are typically written as Python code (rather than Jinja code) and then bound to your template context.
They are typically used for "value conversions".
def val_ago(value, unit="unit"):
if value == 1:
return "{} {} ago".format(value, unit)
else:
return "{} {}s ago".format(value, unit)
Example Template:
{% for second in range(60) %}
{{ second|val_ago(unit="second") }}
{% endfor %}
Output:
0 seconds ago
1 second ago <-- notice
2 seconds ago
...
59 seconds ago
from filters import val_ago
@app.template_filter()
def seconds_ago(val):
return val_ago(val, unit="second")
@app.route('/experiment')
def experiment():
return render_template('seconds.jinja2.html',
seconds=range(60))
... and ...
<ul>
{% for second in seconds %}
<li>{{ second|seconds_ago }}
{% endfor %}
</ul>
Filters are very powerful since they can boil down some complex processing logic into a simple front-end value transformation.
The example we're going to work through now involves converting absolute datetime objects into human-readable relative dates, such as "10 seconds ago", and "3 days ago".
We'll implement this with a pure Python function we'll then bind as a template filter.
jan1 = dt.datetime(2013, 1, 1)
def test_case(expected, **kwargs): #**
val = jan1 - dt.timedelta(**kwargs) #**
human = human_date(val, nowfunc=lambda: jan1)
assert human == expected, human
test_case("1 day ago", days=1)
test_case("2 days ago", days=2)
test_case("5 seconds ago", seconds=5)
test_case("2 minutes ago", seconds=60*2)
test_case("3 hours ago", seconds=60*60*3)
test_case("12/25/2012", days=7)
def human_date(dateval, nowfunc=dt.datetime.now):
now = nowfunc()
delta = now - dateval
days = delta.days
if days == 0:
seconds = delta.seconds
minutes = seconds / 60
hours = minutes / 60
if hours > 0:
return val_ago(hours, unit="hour")
if minutes > 0:
return val_ago(minutes, unit="minute")
return val_ago(seconds, unit="second")
elif 0 < days < 7:
return val_ago(days, unit="day")
else:
return dateval.strftime("%m/%d/%Y")
import datetime as dt
from filters import human_date
app.add_template_filter(human_date)
def _example_dates():
now = dt.datetime.now()
deltas = [ dt.timedelta(seconds=5),
dt.timedelta(seconds=60*60),
dt.timedelta(days=5),
dt.timedelta(days=60)]
dates = [now - delta for delta in deltas]
return dates
@app.route('/datetest')
def datetest():
dates = _example_dates()
return render_template('dates.jinja2.html',
dates=dates)
<ul>
{% for date in dates %}
<li>{{ date }} ({{ date|human_date }})
{% endfor %}
</ul>
with output:
<ul>
<li>2013-02-18 09:40:18.713401 (5 seconds ago)
<li>2013-02-18 08:40:23.713401 (1 hour ago)
<li>2013-02-13 09:40:23.713401 (5 days ago)
<li>2012-12-20 09:40:23.713401 (12/20/2012)
</ul>
{% for row in rows|sort(attribute="date", reverse=True) %}
<tr>
<td><span class="label label-important">{{ row.score }}</span></td>
<td><a href="{{ row.link }}">{{ row.title }}</a></td>
<td><span class="label">{{ row.date|human_date }}</span></td>
</tr>
{% endfor %}
Last piece of the server puzzle for this app is the click redirector.
We'll be able to measure re-submit upvotes by instrumenting insert_article.
But to measure clickthroughs, we need to replace our links in the app with something else.
Shall we brainstorm ideas?
A Flask Route called "/click" that takes a single parameter, url, and tracks a click to that URL. This will utilize a database call called track_click that tracks this URL in our database. Then, directs the user to the appropriate URL via an HTTP redirect.
A Jinja macro called "tracked_link()" that takes a title and URL and generates a link tag to our "/click" route.
@app.route('/click/')
def click():
url = request.args["url"]
track_click(url)
return redirect(url)
{%- macro tracked_link(title, url) -%}
<a href="{{ url_for("click", url=url) }}">{{ title }}</a>
{%- endmacro -%}
... and its usage:
<tr>
<td><span class="label label-important">{{ row.score }}</span></td>
<td>{{ tracked_link(row.title, row.link) }}</td>
<td><span class="label">{{ row.date|human_date }}</span></td>
</tr>
Let's take a 5m break to answer questions / reflect a bit.
So far, all of our development has been "local".
This has lots of benefits:
However, eventually, you want to have a server for your web app -- even if it is only a prototype.
We have no shortage of choices when it comes to where to deploy our Python application.
For simplicity, I'm going to walk you through the deployment of our app on Rackspace Cloud.
Unlike "Shared" hosting environments, Rackspace gives you full control of your deployment Linux operating system, aka "root access".
And unlike "Platforms", you are not locked into using any proprietary deployment tooling or process. My other worry with "Platforms" is that you don't learn anything about how the web really works.
Basically, Rackspace gives you a "virtual private server".
Our Rackspace Nextgen Cloud Server.
Initial access to the server is granted via a "root" account, with a pre-determined password.
From that moment on, most people switch to SSH connections via public/private key pairs.
This has the side benefit of obviating the need for password entry at the command-line.
(Github uses this same trick for read+write Git access.)
In the case of hacknode, I've created a non-root account called shared which will be shared by every person in the class.
We'll add your public keys to this account, and you'll do your deployments in its home directory (/home/shared).
Once you have SSH access, you can use the ssh command as a simple remote job runner.
e.g. ssh hacknode ls /tmp will list the contents of the /tmp directory on the server.
e.g. ssh hacknode ps aux will list all running processes on the server.
Once you have SSH set up correctly and can connect to / run remote commands on a remote server, you are all ready to start scripting deployment.
In the Python community, we use a simple tool called Fabric for this.
Fabric installs a little program called fab into your PATH.
fab looks for a file called fabfile.py, which is written using Fabric's core library. You define tasks that correspond to command-line arguments.
Tasks can actually do pretty much anything, but are typically used for scripting remote machines, e.g. copying files onto the remote machine and executing remote commands.
In requirements.txt:
ipython
Flask
Flask-Script
Fabric
And re-install with pip install -r requirements.txt.
This will help us with deployment later.
from flask.ext.script import Manager
app = Flask(...)
app.debug = True
manager = Manager(app)
if __name__ == "__main__":
manager.run()
$ python app.py
Please provide a command:
runserver Runs the Flask development server i.e. app.run()
shell Runs a Python shell inside Flask application context.
So, e.g., to run on all IPs and port 8000:
$ python app.py runserver --host=0.0.0.0 --port=8000
* Running on http://0.0.0.0:8000/
from fabric.api import *
env.use_ssh_config = True
env.hosts = ["shared@hacknode"]
@task
def list_home():
"""List files in home directory."""
run("ls -lha")
$ fab -l
Available commands:
list_home List files in home directory.
$ fab list_home
[shared@hacknode] Executing task 'list_home'
[shared@hacknode] run: ls -lha
[shared@hacknode] out: total 40K
[shared@hacknode] out: drwxr-xr-x 6 shared shared 4.0K Feb 20 23:33 .
[shared@hacknode] out: drwxr-xr-x 4 root root 4.0K Feb 20 21:19 ..
[shared@hacknode] out: -rw------- 1 shared shared 159 Feb 20 23:17 .bash_history
[shared@hacknode] out: -rw-r--r-- 1 shared shared 220 Feb 20 21:19 .bash_logout
[shared@hacknode] out: -rw-r--r-- 1 shared shared 3.5K Feb 20 21:19 .bashrc
[shared@hacknode] out: drwx------ 2 shared shared 4.0K Feb 20 21:20 .cache
[shared@hacknode] out: drwxrwxr-x 2 shared shared 4.0K Feb 20 23:33 .pip
[shared@hacknode] out: -rw-r--r-- 1 shared shared 675 Feb 20 21:19 .profile
[shared@hacknode] out: drwxr-xr-x 2 shared shared 4.0K Feb 20 21:20 .ssh
[shared@hacknode] out:
Done.
Disconnecting from [email protected]... done.
from fabric.contrib.project import rsync_project
def unique_id():
def sh(cmd): return local(cmd, capture=True)
return "{}__{}".format(sh("whoami"), sh("hostname"))
@task
def print_my_id():
"""Print your unique identifier."""
puts("UNIQUE ID: " + unique_id())
@task
def deploy():
"""Deploy project remotely."""
run("mkdir -p deploys")
rsync_project(remote_dir="deploys/" + unique_id(),
local_dir="./",
exclude=(".git", "rapid-env", "steps", "activate"))
def virtualenv_run(cmd):
run("source rapid-env/bin/activate && {}".format(cmd))
@task
def setup_virtualenv():
"""Set up virtualenv on remote machine."""
with cd("deploys/" + unique_id()):
run("virtualenv rapid-env")
virtualenv_run("pip install -r requirements.txt")
@task
def run_devserver():
"""Run the dev Flask server on remote machine."""
with cd("deploys/" + unique_id()):
virtualenv_run("cd app && python app.py runserver --host=0.0.0.0 --port=8000")
$ fab -l
Available commands:
deploy Deploy project remotely.
list_deploys List deployment directories.
list_home List files in home directory.
print_my_id Print your unique identifier.
run_devserver Run the dev Flask server on remote machine.
setup_virtualenv Set up virtualenv on remote machine.
$ fab deploy
...
$ fab setup_virtualenv
...
$ fab run_devserver
[shared@hacknode] * Running on http://0.0.0.0:8000/
Now, we navigate over to http://hacknode1.alephpoint.com:8000/.
Running a development server is good enough for our early prototyping, but when we ship our app, we want to run in a "real" web server.
Why?
A lightweight bridge between programming languages and web servers.
Originally built just for Python (due to WSGI standard), but now even being used by other languages.
One command and your web application is ready to be plugged into any web server.
uwsgi
# enable HTTP and Python plugins
--plugins=http,python
# use this socket
-s /tmp/uwsgi-hacknode1.sock
# find the Python web app module in this file
--file /home/shared/servers/hacknode1/app/app.py
# look for the variable "app" for the server to run
--callable app
# set the PYTHONHOME directory to our virtualenv
-H /home/shared/servers/hacknode1/rapid-env
supervisor is the most lightweight service runner.
Allows us to run a "long-lived" task, like our web server.
Handles auto-healing, logging, and a simple start/stop/restart user interface.
We'll use it to run our uwsgi instance.
Lives in /etc/supervisor/conf.d/hacknode1.conf:
[program:hacknode1]
command=\
uwsgi \
--plugins=http,python \
-s /tmp/uwsgi-hacknode1.sock \
--file /home/shared/servers/hacknode1/app/app.py --callable app \
-H /home/shared/servers/hacknode1/rapid-env
directory=/home/shared/servers/hacknode1/app
autostart=true
autorestart=true
stdout_logfile=/home/shared/logs/hacknode1.log
redirect_stderr=true
stopsignal=QUIT
nginx is the most lightweight web server available.
Built to support highly concurrent workloads (e.g. 10,000 concurrents).
Simple configuration system.
Python integration outsourced to uwsgi.
Lives in /etc/nginx/sites-enabled/hacknode1:
server {
listen 80;
server_name hacknode1.alephpoint.com;
location / {
try_files $uri @hacknode1;
}
location @hacknode1 {
include uwsgi_params;
# notice: same socket from uwsgi command
uwsgi_pass unix:/tmp/uwsgi-hacknode1.sock;
}
}
Web Request
--> nginx
--> supervisor
--> uwsgi
--> Flask
Your Application Code
Y SO MANY LAYERS?
Lightweight means "right tool for the job", and in this case:
- nginx only knows about serving and proxying HTTP requests
- supervisor only knows about managing long-lived processes
- uwsgi only knows about forwarding HTTP requests to WSGI app servers
- Flask is an app server
So, there may be a lot of layers, but each piece is small and well-understood.
For our team development benefit, I've already configured our hacknode server with this nginx, supervisor, and uwsgi setup (yay sysadmin!)
There are nine identical setups, hacknode{1-9}:
{% for num in range(1, 10) %}
{% set team_name = "hacknode" + num %}
mkdir /home/shared/servers/{{ team_name}};
make_nginx_config {{ team_name }};
make_supervisor_config {{ team_name }};
start_service {{ team_name }};
{% endfor %}
(not actual code, but it's what I did, roughly)
TEAM_NAME = "hacknode2"
# ...
@task
def setup_virtualenv():
"""Set up virtualenv on remote machine."""
with cd("servers/" + TEAM_NAME): # <-- changed
run("virtualenv rapid-env")
virtualenv_run("pip install -r requirements.txt")
@task
def deploy():
"""Deploy project remotely ."""
run("mkdir -p servers") # <-- changed
rsync_project(remote_dir="servers/" + TEAM_NAME, # <-- changed
local_dir="./",
exclude=(".git", "rapid-env", "steps", "activate"))
First, we set up the deployment directory.
$ fab setup_virtualenv
...
$ fab deploy
...
We should now have /home/shared/servers/hacknode2/app/app.py for the app.
We should also have /home/shared/servers/hacknode2/rapid-env for the env.
def supervisor_run(cmd):
sudo("supervisorctl {}".format(cmd), shell=False)
@task
def restart():
"""Restart supervisor service and view some output of log file."""
supervisor_run("restart {}".format(TEAM_NAME))
run("sleep 1")
supervisor_run("tail -800 {}".format(TEAM_NAME))
$ fab restart
[shared@hacknode] Executing task 'restart'
[shared@hacknode] sudo: supervisorctl restart hacknode1
[shared@hacknode] out: hacknode1: stopped
[shared@hacknode] out: hacknode1: started
# ...
[shared@hacknode] sudo: supervisorctl tail -800 hacknode1
# ...
[shared@hacknode] out: uwsgi socket 0 bound to UNIX address /tmp/uwsgi-hacknode1.sock fd 3
[shared@hacknode] out: Python version: 2.7.3 (default, Apr 20 2012, 23:04:22) [GCC 4.6.3]
[shared@hacknode] out: Set PythonHome to /home/shared/servers/hacknode1/rapid-env
[shared@hacknode] out: Python main interpreter initialized at 0x1f06940
[shared@hacknode] out: your server socket listen backlog is limited to 100 connections
[shared@hacknode] out: *** Operational MODE: single process ***
[shared@hacknode] out: WSGI application 0 (mountpoint='') ready on interpreter 0x1f06940 pid: 6265 (default app)
[shared@hacknode] out: *** uWSGI is running in multiple interpreter mode ***
[shared@hacknode] out: spawned uWSGI worker 1 (and the only) (pid: 6265, cores: 1)
# ...
5m discussion to review what we've learned.
Now that we have our web prototype, and a place where we can run our server in production, the last piece that is necessary is to think about where to put our glorious datas.
There are various database types:
- SQL
- NoSQL
- Search
- Dynamo
Even within these types, there are multiple database styles!
- Schema vs Schema-less
- Distributed vs Single-Node
- Dev-friendly vs Sysadmin friendly
We'll do a quick "speed dating" right now.
"Data structures database."
Key-value store. In-memory storage with optional backups to disk.
strings, sets, sorted sets, hashes
Good for: high-performance, low-importance data.
"Document database."
Stores JSON documents, both flat and compound.
Supports indexing for fast queries by using memory.
Has a good replication / sharding story and a great out-of-box experience for developers.
Good for: simple data storage use cases and some high-performance use cases.
"World's simplest SQL database."
Supports full SQL standard, but runs as an "embedded" server.
Good for: development environments, learning SQL, desktop applications.
Not good for servers; no concurrency story.
"World's most advanced open source SQL database."
Supports full SQL standard, "and then some".
Has a great replication story, lots of developer tooling, and tons of performance optimization.
Good for: detailed reporting needs, transactional systems, and also many "common" web app use cases.
Only downsides: some sysadmin burden, some complex tooling/configuration, and you must know SQL.
"World's most advanced open source search engine."
Supports full-text search and complex filtering and faceting.
Recently, has a good replication story, decent developer tooling, and tons of performance optimization.
Good for: any use case where you're dealing with large amounts of text, or where you need to offer a "search" or "drill-down" (filter/refine) interface to users.
"New contender in search engine space."
Supports much of the same functionality as Solr, but was written from ground-up to have a better replication/sharding story.
Good for: large-scale search use cases, e.g. searching the Twitter firehose.
Just don't worry about these databases.
They are for "big data" use cases that are way beyond the needs of your prototype.
They are part of the "upgrade path" for really big apps like Facebook, Advertising Systems, Finance Applications, etc.
Why MongoDB?
Doesn't require me to teach you anything about SQL -- "documents" are an intuitive and even Python-friendly concept.
Awesome developer experience: you install it, and with zero configuration, you're basically ready to store data.
pymongo driver lets you use Python dicts as MongoDB documents: simple!
mongo "shell" is actually a JavaScript shell.
Scales pretty well. We even use it at Parse.ly!
There are pretty straightforward instructions for every operating system at:
But I have also pre-installed it on our hacknode server to save us some time!
$ ssh shared@hacknode
...
$ mongo
...
> use hacknode1
switched to db hacknode1
> db.articles.insert({"title": "Google", "link": "http://google.com"})
> db.articles.find().pretty()
{
"_id" : ObjectId("51277ff21aba565f2bc54c5e"),
"title" : "Google",
"link" : "http://google.com"
}
>>> import pymongo
>>> pymongo.MongoClient()
MongoClient('localhost', 27017)
>>> client = pymongo.MongoClient()
>>> client.hacknode1
Database(MongoClient('localhost', 27017), u'hacknode1')
>>> client.hacknode1.articles
Collection(Database(MongoClient('localhost', 27017), u'hacknode1'), u'articles')
>>> coll = client.hacknode1.articles
>>> coll.find()
<pymongo.cursor.Cursor at 0x2b1b350>
>>> list(coll.find())
[{u'_id': ObjectId('51277ff21aba565f2bc54c5e'),
u'link': u'http://google.com',
u'title': u'Google'}]
from pymongo import MongoClient
TEAM_NAME = "hacknode1"
def get_collection():
return MongoClient()[TEAM_NAME].articles
def insert_article(article):
coll = get_collection()
article["score"] = 0
article["date"] = dt.datetime.now()
print "Inserting ->", article
coll.insert(article)
return True
def insert_article(article):
coll = get_collection()
existing = coll.find_one({"link": article["link"]})
if existing is not None:
print "Found existing, explicit upvoting ->", existing
# updates the document server-side, incrementing score by 5
coll.update({"link": existing["link"]},
{"$inc":
{"score": 5}
})
return True
else:
article["score"] = 0
article["date"] = dt.datetime.now()
print "Inserting ->", article
# inserts a fresh document
coll.insert(article)
return True
def track_click(url):
coll = get_collection()
print "Tracking ->", url
# updates document server-side, incrementing by 1
coll.update({"link": url},
{"$inc":
{"score": 1}
})
return True
def search_articles(query):
print "Searching ->", query
# does a regular expression match server-side
# good for a hacky v1
articles = coll.find({"title":
{"$regex": query}
})
return list(articles)
Web Request
--> nginx
--> uwsgi / supervisor
--> Flask
--> Python / MongoDB
--> Jinja2 Templates
--> Web Response
--> HTML
Browser
--> Requests for Bootstrap CSS/JS
--> Requests for jQuery JS
--> Dynamic Element Modification
Flask is a "microframework" in that it lets you keep the technology small and wire pieces together as you need them.
Larger projects might need to "grow up" into other Python web frameworks. We'll discuss two: Tornado and Django.
Tornado is like a "programmable nginx". Meant for handling 10,000 concurrent requests.
Good for: high-performance API servers.
Difficulties:
- completely different programming model (callback-driven)
- not compatible with all Python libraries
Django is Python's most popular open source web framework.
Deployment is similar to Flask, so performance is similar.
Very popular for software-as-a-service web apps.
Has its own template engine that is less intuitive / powerful vs Jinja2
Built around an ORM (Object-Relational Mapper) that assumes you will use SQL
Even if you use SQL, their ORM is under-powered vs e.g. SQLAlchemy
IMO, a lot of "magic" in the framework that doesn't need to be there
Awesome admin interface built with the ORM
Lots of open source plugins via "middleware"
Good pattern for large applications with multiple "subapps"
Has a built-in "users" and "groups" model for multi-user web apps
More widely used in production, analyzed for security etc.
Service-Oriented Architecture (SOA) allows you to mix-and-match services by defining how they talk to one another.
HTTP and JSON provide a low-cost and easy-to-understand communication protocol between these services.
As your web app grows up, it might make sense to think about it as a "few small apps communicating with well-defined interfaces." This will allow you to use the best tool for the job.
e.g. use Flask for your main app, and Tornado for your API server.
You've learned a lot!
Rapid Web Prototyping doesn't mean jumping right into code.
Static HTML / CSS / JavaScript can short-circuit the user feedback process.
Good-looking UIs can be built by non-designers.
Lightweight tools, like Bootstrap and Flask, can get you to working web app in record time.
Web development isn't magic! It's just putting a few pieces together.
Building a Fake
HTML, CSS, and JavaScript for static clickables, enhanced by Bootstrap and jQuery.
Python SimpleHTTPServer and/or livereload for prototyping the UI.
Fake a backend using public JSON-P services or local .json files as necessary.
Getting Real
Build a local web application with Flask and design your URL routes.
Convert your static clickables to Jinja2 templates.
Use Macros and Filters for code duplication on the front-end.
Shipping It
Set up a remote server in the cloud, e.g. Rackspace Cloud.
Use Fabric to deploy your code to nginx, uwsgi, and supervisor.
Store your first "real" data with MongoDB.
Let's discuss what we've learned!
Use your powers wisely, and always remember...
It's turtles all the way down!