Frozen-Flask¶
Frozen-Flask freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server.
Note: This project used to be called Flask-Static.
Installation¶
Install the extension with one of the following commands:
$ easy_install Frozen-Flask
or alternatively if you have pip installed:
$ pip install Frozen-Flask
or you can get the source code from github.
Context¶
This documentation assumes that you already have a working Flask application. You can run it and test it with the development server:
from myapplication import app
app.run(debug=True)
Frozen-Flask is only about deployment: instead of installing Python, a WGSI server and Flask on your server, you can use Frozen-Flask to freeze your application and only have static HTML files on your server.
Getting started¶
Create a Freezer
instance with your app
object and call its
freeze()
method. Put that in a freeze.py
script
(or call it whatever you like):
from flask_frozen import Freezer
from myapplication import app
freezer = Freezer(app)
if __name__ == '__main__':
freezer.freeze()
This will create a build
directory next to your application’s static
and templates
directories, with your application’s content frozen into
static files.
Note
Frozen-Flask considers it “owns” its build directory. By default, it will silently overwrite files in that directory, and remove those it did not create.
The configuration allows you to change the destination directory, or control what files are removed if at all.
This build will most likely be partial since Frozen-Flask can only guess so much about your application.
Finding URLs¶
Frozen-Flask works by simulating requests at the WSGI level and writing the responses to aptly named files. So it needs to find out which URLs exist in your application.
The following URLs can be found automatically:
- Static files handled by Flask for your application or any of its blueprints.
- Views with no variable parts in the URL, if they accept the
GET
method. - New in version 0.6: Results of calls to
flask.url_for()
made by your application in the request for another URL. In other words, if you useurl_for()
to create links in your application, these links will be “followed”.
This means that if your application has an index page at the URL /
(without parameters) and every other page can be found from there by
recursively following links built with url_for()
, then
Frozen-Flask can discover all URLs automatically and you’re done.
Otherwise, you may need to write URL generators.
URL generators¶
Let’s say that your application looks like this:
@app.route('/')
def products_list():
return render_template('index.html', products=models.Product.all())
@app.route('/product_<int:product_id>/')
def product_details():
product = models.Product.get_or_404(id=product_id)
return render_template('product.html', product=product)
If, for some reason, some products pages are not linked from another page
(or these links are not built by url_for()
), Frozen-Flask will
not find them.
To tell Frozen-Flask about them, write an URL generator and put it after
creating your Freezer
instance and before calling
freeze()
:
@freezer.register_generator
def product_details():
for product in models.Product.all():
yield {'product_id': product.id}
Frozen-Flask will find the URL by calling url_for(endpoint, **values)
where
endpoint
is the name of the generator function and values
is each
dict yielded by the function.
You can specify a different endpoint by yielding a (endpoint, values)
tuple instead of just values
, or you can by-pass url_for
and simply
yield URLs as strings.
Also, generator functions do not have to be Python generators using yield
,
they can be any callable and return any iterable object.
All of these are thus equivalent:
@freezer.register_generator
def product_details(): # endpoint defaults to the function name
# `values` dicts
yield {'product_id': '1'}
yield {'product_id': '2'}
@freezer.register_generator
def product_url_generator(): # Some other function name
# `(endpoint, values)` tuples
yield 'product_details', {'product_id': '1'}
yield 'product_details', {'product_id': '2'}
@freezer.register_generator
def product_url_generator():
# URLs as strings
yield '/product_1/'
yield '/product_2/'
@freezer.register_generator
def product_url_generator():
# Return a list. (Any iterable type will do.)
return [
'/product_1/',
# Mixing forms works too.
('product_details', {'product_id': '2'}),
]
Generating the same URL more than once is okay, Frozen-Flask will build it only once. Having different functions with the same name is generally a bad practice, but still work here as they are only used by their decorators. In practice you will probably have a module for your views and another one for the freezer and URL generators, so having the same name is not a problem.
Testing URL generators¶
The idea behind Frozen-Flask is that you can use Flask directly to develop and test your application. However, it is also useful to test your URL generators and see that nothing is missing, before deploying to a production server.
You can open the newly generated static HTML files in a web browser, but
links probably won’t work. The FREEZER_RELATIVE_URLS
configuration
can fix this, but adds a visible index.html
to the links.
Alternatively, use the run()
method to start an HTTP server on the build result,
so you can check that everything is fine before uploading:
if __name__ == '__main__':
freezer.run(debug=True)
Freezer.run()
will freeze your application before serving and when
the reloader kicks in. But the reloader only watches Python files, not
templates or static files. Because of that, you probably want to use
Freezer.run()
only for testing the URL generators. For everything
else use the usual app.run()
.
Flask-Script may come in handy here.
Controlling What Is Followed¶
Frozen-Flask follows links automatically or with some help from URL generators.
If you want to control what gets followed, then URL generators should be used
with the Freezer’s with_no_argument_rules
and log_url_for
flags.
Disabling these flags will force Frozen-Flask to use URL generators only. The
combination of these three elements determines how much Frozen-Flask will
follow.
Configuration¶
Frozen-Flask can be configured using Flask’s configuration system. The following configuration values are accepted:
FREEZER_BASE_URL
- Full URL your application is supposed to be installed at. This affects
the output of
flask.url_for()
for absolute URLs (with_external=True
) or if your application is not at the root of its domain name. Defaults to'http://localhost/'
. FREEZER_RELATIVE_URLS
If set to
True
, Frozen-Flask will patch the Jinja environment so thaturl_for()
returns relative URLs. Defaults toFalse
. Python code is not affected unless you userelative_url_for()
explicitly. This enables the frozen site to be browsed without a web server (opening the files directly in a browser) but appends a visibleindex.html
to URLs that would otherwise end with/
.New in version 0.10.
FREEZER_DEFAULT_MIMETYPE
The MIME type that is assumed when it can not be determined from the filename extension. If you’re using the Apache web server, this should match the
DefaultType
value of Apache’s configuration. Defaults toapplication/octet-stream
.New in version 0.7.
FREEZER_IGNORE_MIMETYPE_WARNINGS
If set to
True
, Frozen-Flask won’t show warnings if the MIME type returned from the server doesn’t match the MIME type derived from the filename extension. Defaults toFalse
.New in version 0.8.
FREEZER_DESTINATION
- Path to the directory where to put the generated static site. If relative,
interpreted as relative to the application root, next to the
static
andtemplates
directories. Defaults tobuild
. FREEZER_REMOVE_EXTRA_FILES
If set to
True
(the default), Frozen-Flask will remove files in the destination directory that were not built during the current freeze. This is intended to clean up files generated by a previous call toFreezer.freeze()
that are no longer needed. Setting this toFalse
is equivalent to settingFREEZER_DESTINATION_IGNORE
to['*']
.New in version 0.5.
FREEZER_DESTINATION_IGNORE
A list (defaults empty) of
fnmatch
patterns. Files or directories in the destination that match any of the patterns are not removed, even ifFREEZER_REMOVE_EXTRA_FILES
is true. As in.gitignore
files, patterns apply to the whole path if they contain a slash/
, to each slash-separated part otherwise. For example, this could be set to['.git*']
if the destination is a git repository.New in version 0.10.
FREEZER_STATIC_IGNORE
A list (defaults empty) of
fnmatch
patterns. Files served by send_static_file that match any of the patterns are not copied to the build directory. As in.gitignore
files, patterns apply to the whole path if they contain a slash/
, to each slash-separated part otherwise. For example, this could be set to['*.scss']
to stop all SASS files from being frozen.New in version 0.12.
FREEZER_IGNORE_404_NOT_FOUND
If set to
True
(defaultsFalse
), Frozen-Flask won’t stop freezing when a 404 error is returned by your application. In this case, a warning will be printed on stdout and the static page will be generated using your 404 error page handler or flask’s default one. This can be useful during development phase if you have already referenced pages which aren’t written yet.New in version 0.12.
FREEZER_REDIRECT_POLICY
The policy for handling redirects. The default is
'follow'
which means that when a redirect response is encountered it will follow it to get the content from the redirected location.'ignore'
will not stop freezing, but no content will appear in the redirected location.'error'
will raise an exception if a redirect is encountered.New in version 0.13.
FREEZER_SKIP_EXISTING
If set to
True
(defaultsFalse
), Frozen-Flask will skip the generation of files that already exist in the build directory, even if the contents would have been different. Useful if your generation takes up a very long time and you only want to generate new files.New in version 0.14.
Filenames and MIME types¶
For each generated URL, Frozen-Flask simulates a request and saves the content
in a file in the FREEZER_DESTINATION
directory. The filename is
built from the URL. URLs with a trailing slash are interpreted as a directory
name and the content is saved in index.html
.
Query strings are removed from URLs to build filenames. For example,
/lorem/?page=ipsum
is saved to lorem/index.html
. URLs that are only
different by their query strings are considered the same, and they should
return the same response. Otherwise, the behavior is undefined.
Additionally, the extension checks that the filename has an extension that
matches the MIME type given in the Content-Type
HTTP response header.
In case of mismatch, the Content-Type that a static web server will send
will probably not be the one you expect, so Frozen-Flask issues a warning.
For example, the following views are both wrong:
@app.route('/lipsum')
def lipsum():
return '<p>Lorem ipsum, ...</p>'
@app.route('/style.css')
def compressed_css():
return '/* ... */'
as the default Content-Type
in Flask is text/html; charset=utf-8
, but
the MIME types guessed by the Frozen-Flask as well as most web servers from the
filenames are application/octet-stream
and text/css
.
This can be fixed by adding a trailing slash to the URL or serving with the
right Content-Type
:
# Saved as `lipsum/index.html` matches the 'text/html' MIME type.
@app.route('/lipsum/')
def lipsum():
return '<p>Lorem ipsum, ...</p>'
@app.route('/style.css')
def compressed_css():
return '/* ... */', 200, {'Content-Type': 'text/css; charset=utf-8'}
Alternatively, these warnings can be disabled entirely in the configuration.
Character encodings¶
Flask uses Unicode everywhere internally, and defaults to UTF-8 for I/O.
It will send the right Content-Type
header with both a MIME type and
encoding (eg. text/html; charset=utf-8
). Frozen-Flask will try to
preserve MIME types through file extensions, but it can not
preserve the encoding meta-data. You may need to add the right
<meta>
tag to your HTML. (You should anyway).
Flask also defaults to UTF-8 for URLs, so your web server will get URL-encoded UTF-8 HTTP requests. It’s up to you to make sure that it converts these to the native filesystem encoding. Frozen-Flask always writes Unicode filenames.
API reference¶
-
class
flask_frozen.
Freezer
(app=None, with_static_files=True, with_no_argument_rules=True, log_url_for=True)¶ Parameters: - app (Flask instance) – your application or None if you use
init_app()
- with_static_files (boolean) – Whether to automatically generate URLs for static files.
- with_no_argument_rules (boolean) – Whether to automatically generate URLs for URL rules that take no arguments.
- log_url_for (boolean) –
Whether to log calls your app makes to
url_for()
and generate URLs from that.New in version 0.6.
-
all_urls
()¶ Run all generators and yield URLs relative to the app root. May be useful for testing URL generators.
Note
This does not generate any page, so URLs that are normally generated from
url_for()
calls will not be included here.
-
freeze
()¶ Clean the destination and build all URLs from generators.
-
freeze_yield
()¶ Like
freeze()
, but yields information about pages as they are being processed. Yieldsnamedtuples
(url, path)
. This can be used to display progress information, such as printing the information to standard output, or even more sophisticated, e.g. with aprogressbar
:import click with click.progressbar( freezer.freeze_yield(), item_show_func=lambda p: p.url if p else 'Done!') as urls: for url in urls: # everything is already happening, just pass pass
-
init_app
(app)¶ Allow to register an app after the Freezer initialization.
Parameters: app – your Flask application
-
register_generator
(function)¶ Register a function as an URL generator.
The function should return an iterable of URL paths or
(endpoint, values)
tuples to be used asurl_for(endpoint, **values)
.Returns: the function, so that it can be used as a decorator
-
root
¶ Absolute path to the directory Frozen-Flask writes to, ie. resolved value for the
FREEZER_DESTINATION
configuration.
-
serve
(**options)¶ Run an HTTP server on the result of the build.
Parameters: options – passed to flask.Flask.run()
.
- app (Flask instance) – your application or None if you use
-
flask_frozen.
walk_directory
(root, ignore=())¶ Recursively walk the root directory and yield slash-separated paths relative to the root.
Used to implement the URL generator for static files.
Parameters: ignore – A list of fnmatch
patterns. As in.gitignore
files, patterns that contain a slash are matched against the whole path, others against individual slash-separated parts.
-
flask_frozen.
relative_url_for
(endpoint, **values)¶ Like
url_for()
, but returns relative URLs if possible.Absolute URLs (with
_external=True
or to a different subdomain) are unchanged, but eg./foo/bar
becomes../bar
, depending on the current request context’s path. (This, of course, requires a Flask request context.)URLs that would otherwise end with
/
getindex.html
appended, as Frozen-Flask does in filenames. Because of this behavior, this function should only be used with Frozen-Flask, not when running the application inapp.run()
or another WSGI sever.If the
FREEZER_RELATIVE_URLS
configuration is True, Frozen-Flask will automatically patch the application’s Jinja environment so thaturl_for
in templates is this function.
Changelog¶
Version 0.15¶
Released on 2017-06-27.
- Add
Freezer.freeze_yield()
method to make progress reporting easier. (Thanks to Miro Hrončok.)
Version 0.14¶
Released on 2017-03-22.
- Add the
FREEZER_SKIP_EXISTING
configuration to skip generation of files already in the build directory. (Thanks to Antoine Goutenoir.) - Add shared superclass
FrozenFlaskWarning
for all warnings. (Thanks to Miro Hrončok.)
Version 0.12¶
Released on 2015-11-05.
- Add the
FREEZER_IGNORE_404_NOT_FOUND
configuration. (Thanks to Thomas Sarboni.) - Add the
FREEZER_STATIC_IGNORE
configuration. (Thanks to Alex Guerra.) - Fix #36: Support non-default app.config[‘SERVER_NAME’].
Version 0.11¶
Released on 2013-06-13.
- Add Python 3.3 support (requires Flask >= 0.10 and Werkzeug >= 0.9)
- Drop Python 2.5 support
- Fix #30:
relative_url_for()
with a query string or URL fragment.
Version 0.10¶
Released on 2013-03-11.
- Add the
FREEZER_DESTINATION_IGNORE
configuration (Thanks to Jim Gray and Christopher Roach.) - Add the
FREEZER_RELATIVE_URLS
configuration - Add the
relative_url_for()
function.
Version 0.8¶
Released on 2012-01-17.
- Remove query strings from URLs to build a file names. (Should we add configuration to disable this?)
- Raise a warning instead of an exception for MIME type mismatches, and give the option to disable them entirely in the configuration.
Version 0.7¶
Released on 2011-10-20.
- Backward incompatible change: Moved the
flaskext.frozen
package toflask_frozen
. You should change your imports either to that or toflask.ext.frozen
if you’re using Flask 0.8 or more recent. See Flask’s documentation for details. - Added FREEZER_DEFAULT_MIMETYPE
- Switch to tox for testing in multiple Python versions
Version 0.6¶
Released on 2011-07-29.
- Thanks to Glwadys Fayolle for the new logo!
- Frozen-Flask now requires Flask 0.7 or later. Please use previous version of Frozen-Flask if you need previous versions of Flask.
- Support for Flask Blueprints
- Added the
log_url_for
parameter toFreezer
. This makes some URL generators unnecessary since more URLs are discovered automatically. - Bug fixes.
Version 0.5¶
Released on 2011-07-24.
- You can now construct a Freezer and add URL generators without an app, and
register the app later with
Freezer.init_app()
. - The
FREEZER_DESTINATION
directory is created if it does not exist. - New configuration:
FREEZER_REMOVE_EXTRA_FILES
- Warn if an URL generator seems to be missing. (ie. if no URL was generated for a given endpoint.)
- Write Unicode filenames instead of UTF-8. Non-ASCII filenames are often undefined territory anyway.
- Bug fixes.
Version 0.4¶
Released on 2011-06-02.
- Bugfix: correctly unquote URLs to build filenames. Spaces and non-ASCII characters should be %-encoded in URLs but not in frozen filenames. (Web servers do the decoding.)
- Add a documentation section about character encodings.
Version 0.3¶
Released on 2011-05-28.
- URL generators can omit the endpoint and just yield
values
dictionaries. In that case, the name of the generator function is used as the endpoint, just like with Flask views. Freezer.all_urls()
andwalk_directory()
are now part of the public API.
Version 0.2¶
Released on 2011-02-21.
Renamed the project from Flask-Static to Frozen-Flask. While we’re at
breaking API compatibility, flaskext.static.StaticBuilder.build()
is now flaskext.frozen.Freezer.freeze()
and the prefix for configuration
keys is FREEZER_
instead of STATIC_BUILDER_
.
Other names were left unchanged.