Python 3home |
Introduction
A "web framework" is an application or package that facilitates web programming. Server-side apps (for example: a catalog, content search and display, reservation site or most other interactive websites) use a framework to handle the details of the web network request, page display and database input/output -- while freeing the programmer to supply just the logic of how the app will work. Full Stack Web Frameworks A web application consists of layers of components that are configured to work together (i.e., built into a "stack"). Such components may include: * authenticating and identifying users through cookies * handling data input from forms and URLs * reading and writing data to and from persistant storage (e.g. databases) * displaying templates with dynamic data inserted * providing styling to templates (e.g., with css) * providing dynamic web page functionality, as with AJAX The term "full stack developer" used by schools and recruiters refers to a developer who is proficient in all of these areas. Django is possibly the most popular web framework. This session would probably focus on Django, but its configuration and setup requirements are too lengthy for the available time. "Lightweight" Web Frameworks A "lightweight" framework provides the base functionality needed for a server-side application, but allows the user to add other stack components as desired. Such apps are typically easier to get started with because they require less configuration and setup. Flask is a popular lightweight framework with many convenient defaults allows us to get our web application started quickly.
"@app.route()" functions describe what happens when the user visits a particular "page" or URL shown in the decorator.
hello_flask.py
Here is a basic template for a Flask app.
#!/usr/bin/env python
import flask
app = flask.Flask(__name__) # a Flask object
@app.route('/hello') # called when visiting web URL 127.0.0.1:5000/hello
def hello_world():
print('*** DEBUG: inside hello_world() ***')
return '<PRE>Hello, World!</PRE>' # expected to return a string (usu. the HTML to display)
if __name__ == '__main__':
app.run(debug=True, port=5000) # app starts serving in debug mode on port 5000
The first two lines and last two lines will always be present (production apps will omit the debug= and port= arguments). app is an object returned by Flask; we will use it for almost everything our app does. We call it a "god object" because it's always available and contains most of what we need. app.run() starts the Flask application server and causes Flask to wait for new web requests (i.e., when a browser visit the server). @app.route() functions are called when a particular URL is requested. The decorator specifies the string to be found at the end of the URL. For example, the above decorator @app.route('/hello') specifies that the URL to reach the function should be http://localhost:5000/hello The string returned from the function is passed on to Flask and then to the browser on the other end of the web request.
Flask comes complete with its own self-contained app server. We can simply run the app and it begins serving locally. No internet connection is required.
After saving the previous example as a python program named hello_flask.py, run the program from the command line:
$ python hello_flask.py * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat 127.0.0.1 - - [13/Nov/2016 15:58:16] "GET / HTTP/1.1" 200 - *** DEBUG: inside hello_world() ***
The Flask app prints out web server log messages showing the URL requested by each visitor. You can also print error messages directly from the application, and they will appear in the log (these were printed with *** strings, for visibility.)
To see the application serve, open a browser and visit this URL:
http://127.0.0.1:5000/hello
Noting the decorator value '/hello', we have added this value to the http:// URL address shown in the Terminal output. If you omit the /hello from the URL or add a slash at the end (/hello/), you will see Not Found in your browser. This means that Flask couldn't identify the @app.route function to call. Changes to Flask code are detected and cause Flask to restart the server; errors cause it to exit
Whenever you make a change to the Flask code and save your script, the Flask server will restart -- you can see it issue messages to this effect:
* Detected change in '/Users/dblaikie/Dropbox/tech/nyu/advanced_python/solutions/flask/guestbook/guestbook_simple.py', reloading * Restarting with stat * Debugger is active! * Debugger pin code: 161-369-356
If there is an error in your code that prevents it from running, the code will raise an exception and exit.
* Detected change in '/Users/dblaikie/Dropbox/tech/nyu/advanced_python/solutions/flask/guestbook/guestbook_simple.py', reloading * Restarting with stat File "./guestbook_simple.py", line 17 return hello, guestbook_id ' + guestbook_id ^ SyntaxError: EOL while scanning string literal
At that point, the browser will simply report "This site can't be reached" with no other information. Therefore we must keep an eye on the window to see if the latest change broke the script -- fix the error in the script, and then re-run the script in the Terminal.
Once an 'event' function is called, it may return a string, call another function, or redirect to another page.
Return a plain string
A plain string will simply be displayed in the browser. This is the simplest way to display text in a browser.
@app.route('/hello')
def hello_world():
return '<PRE>Hello, World!</PRE>' # expected to return a string (usu. the HTML to display)
Return HTML (also a string)
HTML is simply tagged text, so it is also returned as a string.
@app.route('/hello_template')
def hello_html():
return """
<HTML>
<HEAD>
<TITLE>My Greeting Page</TITLE>
</HEAD>
<BODY>
<H1>Hello, world!</H1>
</BODY>
</HTML>"""
Return an HTML Template (to come)
This method also returns a string, but it is returned from the render_template() function.
@app.route('/hello_html')
def hello_html():
return flask.render_template('response.html') # found in templates/response.html
Return another function call
Functions that aren't intended to return strings but to perform other actions (such as making database changes) can simply call other functions that represent the desired destination:
def hello_html():
return """
<HTML>
<HEAD>
<TITLE>My Greeting Page</TITLE>
</HEAD>
<BODY>
<H1>Hello, world (from another function) !</H1>
</BODY>
</HTML>"""
@app.route('/hello_template')
def hello():
return hello_html()
Because hello() is calling hello_html() in its return statement, whatever is returned from there will be returned from hello(). Redirecting to another program URL with flask.redirect() and flask.url_for()
At the end of a function we can call the flask app again through a page redirect -- that is, to have the app call itself with new parameters.
from datetime import date
@app.route('/sunday_hello')
def sunday_hello():
return "It's Sunday! Take a rest!"
@app.route('/shello')
def hello():
if date.today().strftime('%a') == 'Sun':
return flask.redirect(flask.url_for('sunday_hello'))
else:
return 'Hello, workday (or Saturday)!'
redirect() issues a redirection to a specified URL; this can be http://www.yahoo.com or any desired URL. url_for() simply produces the URL that will call the flask app with /login.
Use flask.url_for() to build links to other apps
This app has three pages; we can build a "closed system" of pages by having each page link to another within the site.
happy_image = 'http://davidbpython.com/advanced_python/python_data/happup.jpg'
sad_image = 'http://davidbpython.com/advanced_python/python_data/sadpup.jpg'
@app.route('/question')
def ask_question():
return """
<HTML>
<HEAD><TITLE>Do you like puppies?</TITLE></HEAD>
<BODY>
<H3>Do you like puppies?</H3>
<A HREF="{}">arf!</A><BR>
<A HREF="{}">I prefer cats...</A>
</BODY>
</HTML>""".format(flask.url_for('yes'), flask.url_for('no'))
@app.route('/yes')
def yes():
return """
<HTML>
<HEAD><TITLE>C'mere Boy!</TITLE></HEAD>
<BODY>
<H3>C'mere, Boy!</H3>
<IMG SRC="{}"><BR>
<BR>
Change your mind? <A HREF="{}">Let's try again.</A>
</BODY>
</HTML>""".format(happy_image, flask.url_for('ask_question'))
@app.route('/no')
def no():
return """
<HTML>
<HEAD><TITLE>Aww...</TITLE></HEAD>
<BODY>
<H3>Aww...really?</H3>
<IMG SRC="{}"><BR>
<BR>
Change your mind? <A HREF="{}">Let's try again.</A>
</BODY>
</HTML>""".format(sad_image, flask.url_for('ask_question'))
Use {{ varname }} to create template tokens and flask.render_template() to insert to them.
HTML pages are rarely written into Flask apps; instead, we use standalone template files. The template files are located in a templates directory placed in the same directory as your Flask script.
question.html
<HTML> <HEAD><TITLE>Do you like puppies?</TITLE></HEAD> <BODY> <H3>Do you like puppies?</H3> <A HREF="{{ yes_link }}">arf!</A><BR> <A HREF="{{ no_link }}">I prefer cats...</A> </BODY> </HTML>
puppy.html
<HTML> <HEAD><TITLE><{{ title_message }}</TITLE></HEAD> <BODY> <H3>{{ title_message }}</H3> <IMG SRC="{{ puppy_image }}"><BR> <BR> Change your mind? <A HREF="{{ question_link }}">Let's try again.</A> </BODY> </HTML>
puppy_question.py
happy_image = 'http://davidbpython.com/advanced_python/python_data/happup.jpg'
sad_image = 'http://davidbpython.com/advanced_python/python_data/sadpup.jpg'
@app.route('/question')
def ask_question():
return flask.render_template('question.html',
yes_link=flask.url_for('yes'),
no_link=flask.url_for('no'))
@app.route('/yes')
def yes():
return flask.render_template('puppy.html',
puppy_image=happy_image,
question_link=flask.url_for('ask_question'),
title_message='Cmere, boy!')
@app.route('/no')
def no():
return flask.render_template('puppy.html',
puppy_image=sad_image,
question_link=flask.url_for('ask_question'),
title_message='Aww... really?')
Use {% %} to embed Python code for looping, conditionals and some functions/methods from within the template.
Template document: "template_test.html"
<!DOCTYPE html> <html lang="en"> <head> <title>Important Stuff</title> </head> <body> <h1>Important Stuff</h1> Today's magic number is {{ number }}<br><br> Today's strident word is {{ word.upper() }}<br><br> Today's important topics are:<br> {% for item in mylist %} {{ item }}<br> {% endfor %} <br><br> {% if reliability_warning %} WARNING: this information is not reliable {% endif %} </body> </html>
Flask code
@app.route('template_test')
def template_test():
return flask.render_template('template_test.html', number=1035,
word='somnolent',
mylist=['children', 'animals', 'bacteria'],
reliability_warning=True)
As before, {{ variable }} can used for variable insertions, as well as instance attributes, method and function calls
{% for this in that %} can be used for 'if' tests, looping with 'for' and other basic control flow
Input from a page can come from a link URL, or from a form submission.
name_question.html
<HTML> <HEAD> </HEAD> <BODY> What is your name?<BR> <FORM ACTION="{{ url_for('greet_name') }}" METHOD="post"> <INPUT NAME="name" SIZE="20"> <A HREF="{{ url_for('greet_name') }}?no_name=1">I don't have a name</A> <INPUT TYPE="submit" VALUE="tell me!"> </FORM> </BODY> </HTML>
@app.route('/name_question')
def ask_name():
return flask.render_template('name_question.html')
@app.route('/greet', methods=['POST', 'GET'])
def greet_name():
name = flask.request.form.get('name') # from a POST (form with 'method="POST"')
no_name = flask.request.args.get('no_name') # from a GET (URL)
if name:
msg = 'Hello, {}!'.format(name)
elif no_name:
msg = 'You are anonymous. I respect that.'
else:
raise ValueError('\nraised error: no "name" or "no_name" params passed in request')
return '<PRE>{}</PRE>'.format(msg)
Many times we want to apply the same HTML formatting to a group of templates -- for example the <head> tag, which my include css formatting, javascript, etc.
We can do this with base templates:
{% extends "base.html" %} # 'base.html' can contain HTML from another template <h1>Special Stuff</h1> Here is some special stuff from the world of news.
The base template "surrounds" any template that imports it, inserting the importing template at the {% block body%} tag:
<html> <head> </head> <body> <div class="container"> {% block body %} <H1>This is the base template default body.</H1> {% endblock %} </div> </body> </html>
There are many other features of Jinja2 as well as ways to control the API, although I have found the above features to be adequate for my purposes.
Sessions (usually supported by cookies) allow Flask to identify a user between requests (which are by nature "anonymous").
When a session is set, a cookie with a specific ID is passed from the server to the browser, which then returns the cookie on the next visit to the server. In this way the browser is constantly re-identifying itself through the ID on the cookie. This is how most websites keep track of a user's visits.
import flask
app = flask.Flask(__name__)
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' # secret key
@app.route('/index')
def hello_world():
# see if the 'login' link was clicked: set a session ID
user_id = flask.request.args.get('login')
if user_id:
flask.session['user_id'] = user_id
is_session = True
# else see if the 'logout' link was clicked: clear the session
elif flask.request.args.get('logout'):
flask.session.clear()
# else see if there is already a session cookie being passed: retrieve the ID
else:
# see if a session cookie is already active between requests
user_id = flask.session.get('user_id')
# tell the template whether we're logged in (user_id is a numeric ID, or None)
return flask.render_template('session_test.html', is_session=user_id)
if __name__ == '__main__':
app.run(debug=True, port=5001) # app starts serving in debug mode on port 5001
session_test.html
<!DOCTYPE html> <html lang="en"> <head> <title>Session Test</title> </head> <body> <h1>Session Test</h1> {% if is_session %} <font color="green">Logged In</font> {% else %} <font color="red">Logged Out</font> {% endif %} <br><br> <a href="index?login=True">Log In</a><br> <a href="index?logout=True">Log Out</a><br> </body> </html>
Configuration values are set to control how Flask works as well as to be set and referenced by an individual application.
Flask sets a number of variables for its own behavior, among them DEBUG=True to display errors to the browser, and SECRET_KEY='!jmNZ3yX R~XWX/r]LA098j/,?RTHH' to set a session cookie's secret key. A list of Flask default configuration values is here. Retrieving config values
value = app.config['SERVER_NAME']
Setting config values individually
app.config['DEBUG'] = True
Setting config values from a file
app.config.from_pyfile('flaskapp.cfg')
Such a file need only contain python code that sets uppercased constants -- these will be added to the config. Setting config values from a configuration Object Similarly, the class variables defined within a custom class can be read and applied to the config with app.config.from_object(). Note in the example below that we can use inheritance to distribute configs among several classes, which can aid in organization and/or selection:
In a file called configmodule.py:
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite://:memory:'
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config):
DEBUG = True
class TestingConfig(Config):
TESTING = True
In the flask script:
app.config.from_object('configmodule.ProductionConfig')
Environment Variables are system-wide values that are set by the operating system and apply to all applications. They can also be set by individual applications.
The OpenShift web container sets a number of environment variables, among them OPENSHIFT_LOG_DIR for log files and OPENSHIFT_DATA_DIR for data files. Flask employs jinja2 templates. A list of Openshift environment variables can be found here.
An important caveat regarding web security: Flask is not considered to be a secure approach to handling sensitive data.
...at least, that was the opinion of a former student, a web programmer who worked for Bank of America, about a year ago -- they evaluated Flask and decided that it was not reliable and could have security vulnerabilities. His team decided to use CGI -- the baseline protocol for handling web requests. Any framework is likely to have vulnerabilities -- only careful research and/or advice of a professional can ensure reliable privacy. However for most applications security is not a concern -- you will simply want to avoid storing sensitive data on a server without considering security.
Keep these Flask-specific errors in mind.
Page not found
URLs specified in @app.route() functions should not end in a trailing slash, and URLs entered into browsers must match
@app.route(/'hello')
If you omit a trailing slash from this path but include one in the URL, the browser will respond that it can't find the page. What's worse, some browsers sometimes try to 'correct' your URL entries, so if you type a URL with a trailing slash and get "Page not found", the next time you type it differently (even if correctly) the browser may attempt to "correct" it to the way you typed it the first time (i.e., incorrectly). This can be extremely frustrating; the only remedy I have found is to clear the browser's browsing data.
Functions must return strings (or redirect to another function or URL); they do not print page responses.
@app.route(/'hello')
def hello():
return 'Hello, world!' # not print('Hello, world!')
Each routing function expects a string to be returned -- so the function must do one of these: 1) return a string (this string will be displayed in the browser) 2) call another @app.route() function that will return a string 3) issue a URL redirect (described later) Method not allowed usually means that a form was submitting that specifies method="POST" but the @app.route decorator doesn't specify methods=['POST']. See "Reading args from URL or Form Input", above.
name_question.html
<HTML> <HEAD> </HEAD> <BODY> What is your name?<BR> <FORM ACTION="{{ url_for('greet_name') }}" METHOD="post"> <INPUT NAME="name" SIZE="20"> <A HREF="{{ url_for('greet_name') }}?no_name=1">I don't have a name</A> <INPUT TYPE="submit" VALUE="tell me!"> </FORM> </BODY> </HTML>
If the form above submits data as a "post", the app.route() function would need to specify this as well:
@app.route('/greet', methods=['POST', 'GET'])
def greet_name():
...