Bundling static assets via a Pyramid console script

Modern applications often require some kind of build step for bundling static assets for either a development or production environment. This recipe illustrates how to build a console script that can help with this task. It also tries to satisfy typical requirements:

  • Frontend source code can be distributed as a Python package.

  • The source code's repository and site-packages are not written to during the build process.

  • Make it possible to provide a plug-in architecture within an application through multiple static asset packages.

  • The application's home directory is the destination of the build process to facilitate HTTP serving by a web server.

  • Flexible - Allows any frontend toolset (Yarn, Webpack, Rollup, etc.) for JavaScript, CSS, and image bundling to compose bigger pipelines.

Demo

This recipe includes a demo application. The source files are located on GitHub:

https://github.com/Pylons/pyramid_cookbook/tree/master/docs/static_assets/bundling

The demo was generated from the Pyramid starter cookiecutter.

Inside the directory bundling are two directories:

  • bundling_example is the Pyramid app generated from the cookiecutter with some additional files and modifications as described in this recipe.

  • frontend contains the frontend source code and files.

You can generate a project from the starter cookiecutter, install it, then follow along with the rest of this recipe. If you run into any problems, compare your project with the demo project source files to see what might be amiss.

Requirements

This recipe and the demo application both require Yarn and NodeJS 8.x packages to be installed.

Configure Pyramid

First we need to tell Pyramid to serve static content from an additional build directory. This is useful for development. In production, often this will be handled by Nginx.

In your configuration file, in the [app:main] section, add locations for the build process:

# build result directory
statics.dir = %(here)s/static
# intermediate directory for build process
statics.build_dir = %(here)s/static_build

In your application's routes, add a static asset view and an asset override configuration:

 1import pathlib
 2# after default static view add bundled static support
 3config.add_static_view(
 4    "static_bundled", "static_bundled", cache_max_age=1
 5)
 6path = pathlib.Path(config.registry.settings["statics.dir"])
 7# create the directory if missing otherwise pyramid will not start
 8path.mkdir(exist_ok=True)
 9config.override_asset(
10    to_override="yourapp:static_bundled/",
11    override_with=config.registry.settings["statics.dir"],
12)

Now in your templates, reference the built and bundled static assets.

<script src="{{ request.static_url('yourapp:static_bundled/some-package.min.js') }}"></script>

Console script

Create a directory scripts at the root of your application. Add an empty __init__.py file to this sub-directory so that it becomes a Python package. Also in this sub-directory, create a file build_static_assets.py to serve as a console script to compile assets, with the following code.

 1import argparse
 2import json
 3import logging
 4import os
 5import pathlib
 6import shutil
 7import subprocess
 8import sys
 9
10import pkg_resources
11from pyramid.paster import bootstrap, setup_logging
12
13log = logging.getLogger(__name__)
14
15
16def build_assets(registry, *cmd_args, **cmd_kwargs):
17    settings = registry.settings
18    build_dir = settings["statics.build_dir"]
19    try:
20        shutil.rmtree(build_dir)
21    except FileNotFoundError as exc:
22        log.warning(exc)
23    # your application frontend source code and configuration directory
24    # usually the containing main package.json
25    assets_path = os.path.abspath(
26        pkg_resources.resource_filename("bundling_example", "../../frontend")
27    )
28    # copy package static sources to temporary build dir
29    shutil.copytree(
30        assets_path,
31        build_dir,
32        ignore=shutil.ignore_patterns(
33            "node_modules", "bower_components", "__pycache__"
34        ),
35    )
36    # configuration files/variables can be picked up by webpack/rollup/gulp
37    os.environ["FRONTEND_ASSSET_ROOT_DIR"] = settings["statics.dir"]
38    worker_config = {'frontendAssetRootDir': settings["statics.dir"]}
39    worker_config_file = pathlib.Path(build_dir) / 'pyramid_config.json'
40
41    with worker_config_file.open('w') as f:
42        f.write(json.dumps(worker_config))
43    # your actual build commands to execute:
44
45    # download all requirements
46    subprocess.run(["yarn"], env=os.environ, cwd=build_dir, check=True)
47    # run build process
48    subprocess.run(["yarn", "build"], env=os.environ, cwd=build_dir, check=True)
49
50
51def parse_args(argv):
52    parser = argparse.ArgumentParser()
53    parser.add_argument("config_uri", help="Configuration file, e.g., development.ini")
54    return parser.parse_args(argv[1:])
55
56
57def main(argv=sys.argv):
58    args = parse_args(argv)
59    setup_logging(args.config_uri)
60    env = bootstrap(args.config_uri)
61    request = env["request"]
62    build_assets(request.registry)

Edit your application's setup.py to create a shell script when you install your application that you will use to start the compilation process.

 1setup(
 2    name='yourapp',
 3    ....
 4    install_requires=requires,
 5    entry_points={
 6        'paste.app_factory': [
 7            'main = channelstream_landing:main',
 8        ],
 9        'console_scripts': [
10            'yourapp_build_statics = yourapp.scripts.build_static_assets:main',
11        ]
12    },
13)

Install your app

Run pip install -e . again to register the console script.

Now you can configure/run your frontend pipeline with webpack/gulp/rollup or other solution.

Compile static assets

Finally we can compile static assets from the frontend and write them into our application.

Run the command:

yourapp_build_statics development.ini

This starts the build process. It creates a fresh static directory in the same location as your application's ini file. The directory should contain all the build process files ready to be served on the web.

You can retrieve variables from your Pyramid application in your Node build configuration files:

destinationRootDir = process.env.FRONTEND_ASSSET_ROOT_DIR

You can view a generated pyramid_config.json file in your Node script for additional information.