This concept is very much like .jar
or .war
archives in Java.
NOTE: The built
.pyz
zipapp can run on both Python 2 & 3 but you can only build.pyz
zipapps with Python 3.5 or later.
There is a single subdirectory called flaskr
in the beginning. It's an example Flask app that I'm going to package into a Python zipapp.
$ tree -L 2 --dirsfirst -F .
.
└── flaskr/
├── flaskr/
├── flaskr.egg-info/
├── venv/
├── LICENSE
├── MANIFEST.in
├── README.rst
├── requirements.txt
└── setup.py
4 directories, 5 files
Now let's create a new Python virtual environment next to flaskr
subdirectory and install Flask
and gunicorn
into the new virtual environment:
$ virtualenv -p python3 venv
$ source ./venv/bin/activate
$ pip install Flask gunicorn
Now there should be two subdirectories: flaskr
and venv
:
$ tree -L 1 --dirsfirst -F .
.
├── flaskr/
└── venv/
2 directories, 0 files
Let's create a new directory called app
which will be packaged into a zipapp as a whole.
In order to create a zipapp you must have all dependencies installed in the same directory as your application source code (e.g our app
directory), not in a virtualenv. I'm going to create a requirements.txt
file to pin the versions of each installed dependent Python package that I've just installed in my virtualenv and then install all of them into the new app
directory.
Alternatively, you could skip the virtualenv step and run pip install -t ./app Flask gunicorn
if you don't want to pin versions.
$ mkdir app
$ cd app
$ pip freeze > requirements.txt
$ pip install -t . -r requirements.txt
Let's copy our application code into the new app
directory:
$ cp -r ../flaskr .
Or install it if you have a nice distributable Python package that has a setup.py
:
$ pip install -t . ../flaskr
To save disk space you can remove extra pip
files and cache before creating a zipapp:
$ rm -rf ./__pycache__ ./*.dist-info
$ cd ..
Now your project should look like this.
$ tree -L 2 --dirsfirst -F .
.
├── app/
│ ├── click/
│ ├── flask/
│ ├── gunicorn/
│ ├── jinja2/
│ ├── markupsafe/
│ ├── werkzeug/
│ ├── itsdangerous.py
│ └── requirements.txt
├── flaskr/
│ ├── flaskr/
│ ├── flaskr.egg-info/
│ ├── venv/
│ ├── LICENSE
│ ├── MANIFEST.in
│ ├── README.rst
│ ├── requirements.txt
│ └── setup.py
└── venv/
├── bin/
├── include/
├── lib/
└── pip-selfcheck.json
15 directories, 8 files
Zipapp module command has this syntax:
$ python3 -m zipapp APP_DIR -m ENTRYPOINT_MODULE:ENTRYPOINT_FUNCTION -p PYTHON_INTERPRETER
I'm going to use gunicorn
to serve my Flask app because it's pure Python and will work accross any supported platform without worrying about binary compilation (e.g. uwsgi
).
When you run gunicorn
command on the command line this is the entrypoint that gets called:
gunicorn.app.wsgiapp:run
So the command line flag for my zipapp entrypoint will be -m 'gunicorn.app.wsgiapp:run'
.
I want to use Python 3 so I also set the interpreter with -p '/usr/bin/env python3'
.
Let's run our zipapp command
$ python3 -m zipapp app -m 'gunicorn.app.wsgiapp:run' -p '/usr/bin/env python3'
You should get an executable .pyz
archive with all dependencies bundled inside:
$ ls -lh app.pyz
-rwxr--r-- 1 user user 4.0M Dec 18 09:38 app.pyz
$ file app.pyz
app.pyz: a /usr/bin/env python3 script executable (binary data)
In order to run my app I have to provide the entrypoint for gunicorn
like I would run any WSGI Python app. In my case it's flaskr:app
:
$ ./app.pyz flaskr:app
[2017-12-18 09:38:35 +0200] [39081] [INFO] Starting gunicorn 19.7.1
[2017-12-18 09:38:35 +0200] [39081] [INFO] Listening at: http://127.0.0.1:8000 (39081)
[2017-12-18 09:38:35 +0200] [39081] [INFO] Using worker: sync
[2017-12-18 09:38:35 +0200] [39084] [INFO] Booting worker with pid: 39084
Hooray, it's running! 🎉
Let's say I don't want to set my flaskr:app
entrypoint each time I run the .pyz
archive.
You can do that by creating a custom app
entrypoint app/__main__.py
with this content:
# -*- coding: utf-8 -*-
import sys
from gunicorn.app.wsgiapp import run
sys.argv.append('flaskr:app')
sys.exit(run())
The repackage your app
$ python3 -m zipapp app -p '/usr/bin/env python3'
And run it
$ ./app.pyz
[2017-12-18 13:35:30 +0200] [44258] [INFO] Starting gunicorn 19.7.1
[2017-12-18 13:35:30 +0200] [44258] [INFO] Listening at: http://127.0.0.1:8000 (44258)
[2017-12-18 13:35:30 +0200] [44258] [INFO] Using worker: sync
[2017-12-18 13:35:30 +0200] [44261] [INFO] Booting worker with pid: 44261