# coding: utf8
"""
flask_weasyprint
~~~~~~~~~~~~~~~~
Flask-WeasyPrint: Make PDF in your Flask app with WeasyPrint.
:copyright: (c) 2012 by Simon Sapin.
:license: BSD, see LICENSE for more details.
"""
import weasyprint
from flask import request, current_app
from werkzeug.test import Client, ClientRedirectError
from werkzeug.wrappers import Response
try:
import urlparse
except ImportError: # Python 3
from urllib import parse as urlparse
try:
unicode
except NameError: # Python 3
unicode = str
VERSION = '0.5'
__all__ = ['VERSION', 'make_flask_url_dispatcher', 'make_url_fetcher',
'HTML', 'CSS', 'render_pdf']
DEFAULT_PORTS = frozenset([('http', 80), ('https', 443)])
[docs]def make_flask_url_dispatcher():
"""Return an URL dispatcher based on the current :ref:`request context
<flask:request-context>`.
You generally don’t need to call this directly.
The context is used when the dispatcher is first created but not
afterwards. It is not required after this function has returned.
Dispatch to the context’s app URLs below the context’s root URL.
If the app has a ``SERVER_NAME`` :ref:`config <flask:config>`, also
accept URLs that have that domain name or a subdomain thereof.
"""
def parse_netloc(netloc):
"""Return (hostname, port)."""
parsed = urlparse.urlsplit('http://' + netloc)
return parsed.hostname, parsed.port
app = current_app._get_current_object()
root_path = request.script_root
server_name = app.config.get('SERVER_NAME')
if server_name:
hostname, port = parse_netloc(server_name)
def accept(url):
"""Accept any URL scheme; also accept subdomains."""
return url.hostname is not None and (
url.hostname == hostname or
url.hostname.endswith('.' + hostname))
else:
scheme = request.scheme
hostname, port = parse_netloc(request.host)
if (scheme, port) in DEFAULT_PORTS:
port = None
def accept(url):
"""Do not accept subdomains."""
return (url.scheme, url.hostname) == (scheme, hostname)
def dispatch(url_string):
if isinstance(url_string, bytes):
url_string = url_string.decode('utf8')
url = urlparse.urlsplit(url_string)
url_port = url.port
if (url.scheme, url_port) in DEFAULT_PORTS:
url_port = None
if accept(url) and url_port == port and url.path.startswith(root_path):
netloc = url.netloc
if url.port and not url_port:
netloc = netloc.rsplit(':', 1)[0] # remove default port
base_url = '%s://%s%s' % (url.scheme, netloc, root_path)
path = url.path[len(root_path):]
if url.query:
path += '?' + url.query
# Ignore url.fragment
return app, base_url, path
return dispatch
[docs]def make_url_fetcher(dispatcher=None,
next_fetcher=weasyprint.default_url_fetcher):
"""Return an function suitable as a ``url_fetcher`` in WeasyPrint.
You generally don’t need to call this directly.
If ``dispatcher`` is not provided, :func:`make_flask_url_dispatcher`
is called to get one. This requires a request context.
Otherwise, it must be a callable that take an URL and return either
``None`` or a ``(wsgi_callable, base_url, path)`` tuple. For None
``next_fetcher`` is used. (By default, fetch normally over the network.)
For a tuple the request is made at the WSGI level.
``wsgi_callable`` must be a Flask application or another WSGI callable.
``base_url`` is the root URL for the application while ``path``
is the path within the application.
Typically ``base_url + path`` is equal or equivalent to the passed URL.
"""
if dispatcher is None:
dispatcher = make_flask_url_dispatcher()
def flask_url_fetcher(url):
redirect_chain = set()
while 1:
result = dispatcher(url)
if result is None:
return next_fetcher(url)
app, base_url, path = result
client = Client(app, response_wrapper=Response)
if isinstance(path, unicode):
# TODO: double-check this. Apparently Werzeug %-unquotes bytes
# but not Unicode URLs. (IRI vs. URI or something.)
path = path.encode('utf8')
response = client.get(path, base_url=base_url)
if response.status_code == 200:
return dict(
string=response.data,
mime_type=response.mimetype,
encoding=response.charset,
redirected_url=url)
# The test client can follow redirects, but do it ourselves
# to get access to the redirected URL.
elif response.status_code in (301, 302, 303, 305, 307):
redirect_chain.add(url)
url = response.location
if url in redirect_chain:
raise ClientRedirectError('loop detected')
else:
raise ValueError('Flask-WeasyPrint got HTTP status %s for %s%s'
% (response.status, base_url, path))
return flask_url_fetcher
def _wrapper(class_, *args, **kwargs):
if args:
guess = args[0]
args = args[1:]
else:
guess = kwargs.pop('guess', None)
if guess is not None and not hasattr(guess, 'read'):
# Assume a (possibly relative) URL
guess = urlparse.urljoin(request.url, guess)
if 'string' in kwargs and 'base_url' not in kwargs:
# Strings do not have an "intrinsic" base URL, use the request context.
kwargs['base_url'] = request.url
kwargs['url_fetcher'] = make_url_fetcher()
return class_(guess, *args, **kwargs)
[docs]def HTML(*args, **kwargs):
"""Like `weasyprint.HTML()
<http://weasyprint.org/using/#the-weasyprint-html-class>`_ but:
* :func:`make_url_fetcher` is used to create an ``url_fetcher``
* If ``guess`` is not a file object, it is an URL relative to the current
request context.
This means that you can just pass a result from :func:`flask.url_for`.
* If ``string`` is passed, ``base_url`` defaults to the current
request’s URL.
This requires a Flask request context.
"""
return _wrapper(weasyprint.HTML, *args, **kwargs)
[docs]def CSS(*args, **kwargs):
return _wrapper(weasyprint.CSS, *args, **kwargs)
CSS.__doc__ = HTML.__doc__.replace('HTML', 'CSS').replace('html', 'css')
[docs]def render_pdf(html, stylesheets=None, download_filename=None):
"""Render a PDF to a response with the correct ``Content-Type`` header.
:param html:
Either a :class:`weasyprint.HTML` object or an URL to be passed
to :func:`flask_weasyprint.HTML`. The latter case requires
a request context.
:param stylesheets:
A list of user stylesheets, passed to
:meth:`~weasyprint.HTML.write_pdf`
:param download_filename:
If provided, the ``Content-Disposition`` header is set so that most
web browser will show the "Save as…" dialog with the value as the
default filename.
:returns: a :class:`flask.Response` object.
"""
if not hasattr(html, 'write_pdf'):
html = HTML(html)
pdf = html.write_pdf(stylesheets=stylesheets)
response = current_app.response_class(pdf, mimetype='application/pdf')
if download_filename:
response.headers.add('Content-Disposition', 'attachment',
filename=download_filename)
return response