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.